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) 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.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}
51
52/// Outcome label for a Polymarket market token.
53///
54/// Free-form string from the API (e.g. "Yes", "No", "Up", "Down").
55/// Every Polymarket market has exactly two outcome tokens; this holds
56/// whichever label the API assigns to one of them.
57#[repr(C)]
58#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
59pub struct PolymarketOutcome(Ustr);
60
61impl PolymarketOutcome {
62    #[must_use]
63    pub fn yes() -> Self {
64        Self(Ustr::from("Yes"))
65    }
66
67    #[must_use]
68    pub fn no() -> Self {
69        Self(Ustr::from("No"))
70    }
71
72    #[must_use]
73    pub fn up() -> Self {
74        Self(Ustr::from("Up"))
75    }
76
77    #[must_use]
78    pub fn down() -> Self {
79        Self(Ustr::from("Down"))
80    }
81
82    #[must_use]
83    pub const fn inner(&self) -> Ustr {
84        self.0
85    }
86
87    #[must_use]
88    pub fn as_str(&self) -> &str {
89        self.0.as_str()
90    }
91}
92
93impl Debug for PolymarketOutcome {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "\"{}\"", self.0)
96    }
97}
98
99impl Display for PolymarketOutcome {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{}", self.0)
102    }
103}
104
105impl From<&str> for PolymarketOutcome {
106    fn from(value: &str) -> Self {
107        Self(Ustr::from(value))
108    }
109}
110
111impl From<Ustr> for PolymarketOutcome {
112    fn from(value: Ustr) -> Self {
113        Self(value)
114    }
115}
116
117/// Order side on the Polymarket CLOB.
118#[derive(
119    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
120)]
121#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
122#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
123pub enum PolymarketOrderSide {
124    Buy,
125    Sell,
126}
127
128/// Liquidity side for fills.
129#[derive(
130    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
131)]
132#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
133#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
134pub enum PolymarketLiquiditySide {
135    Maker,
136    Taker,
137}
138
139/// Order type (time-in-force variant) on the Polymarket CLOB.
140#[derive(
141    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
142)]
143pub enum PolymarketOrderType {
144    FOK,
145    /// Immediate or cancel.
146    FAK,
147    GTC,
148    GTD,
149}
150
151/// WebSocket event type for user channel messages.
152#[derive(
153    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
154)]
155#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
156#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
157pub enum PolymarketEventType {
158    Placement,
159    /// Emitted for a match.
160    Update,
161    Cancellation,
162    Trade,
163}
164
165/// Order status on the Polymarket CLOB.
166#[derive(
167    Clone, Copy, Debug, PartialEq, Eq, Hash, StrumDisplay, EnumString, Serialize, Deserialize,
168)]
169#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
170#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
171pub enum PolymarketOrderStatus {
172    Invalid,
173    Live,
174    /// Marketable but subject to matching delay.
175    Delayed,
176    Matched,
177    /// Marketable but failure delaying, placement not successful.
178    Unmatched,
179    Canceled,
180    CanceledMarketResolved,
181}
182
183/// Trade settlement status on the Polymarket exchange.
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 PolymarketTradeStatus {
190    /// Sent to the executor service for on-chain submission.
191    Matched,
192    /// Mined on-chain, no finality threshold yet.
193    Mined,
194    /// Strong probabilistic finality achieved.
195    Confirmed,
196    /// Transaction failed, being retried by the operator.
197    Retrying,
198    /// Permanently failed, no more retries.
199    Failed,
200}
201
202impl PolymarketTradeStatus {
203    /// Returns `true` if this status represents a finalized trade.
204    #[must_use]
205    pub const fn is_finalized(&self) -> bool {
206        matches!(self, Self::Mined | Self::Confirmed)
207    }
208}
209
210impl From<PolymarketOrderSide> for OrderSide {
211    fn from(value: PolymarketOrderSide) -> Self {
212        match value {
213            PolymarketOrderSide::Buy => Self::Buy,
214            PolymarketOrderSide::Sell => Self::Sell,
215        }
216    }
217}
218
219impl TryFrom<OrderSide> for PolymarketOrderSide {
220    type Error = anyhow::Error;
221
222    fn try_from(value: OrderSide) -> anyhow::Result<Self> {
223        match value {
224            OrderSide::Buy => Ok(Self::Buy),
225            OrderSide::Sell => Ok(Self::Sell),
226            _ => anyhow::bail!("Invalid `OrderSide` for Polymarket: {value:?}"),
227        }
228    }
229}
230
231impl From<PolymarketOrderSide> for AggressorSide {
232    fn from(value: PolymarketOrderSide) -> Self {
233        match value {
234            PolymarketOrderSide::Buy => Self::Buyer,
235            PolymarketOrderSide::Sell => Self::Seller,
236        }
237    }
238}
239
240impl From<PolymarketOrderType> for TimeInForce {
241    fn from(value: PolymarketOrderType) -> Self {
242        match value {
243            PolymarketOrderType::GTC => Self::Gtc,
244            PolymarketOrderType::GTD => Self::Gtd,
245            PolymarketOrderType::FOK => Self::Fok,
246            // Fill-And-Kill is equivalent to Immediate-Or-Cancel
247            PolymarketOrderType::FAK => Self::Ioc,
248        }
249    }
250}
251
252impl TryFrom<TimeInForce> for PolymarketOrderType {
253    type Error = anyhow::Error;
254
255    fn try_from(value: TimeInForce) -> anyhow::Result<Self> {
256        match value {
257            TimeInForce::Gtc => Ok(Self::GTC),
258            TimeInForce::Gtd => Ok(Self::GTD),
259            TimeInForce::Fok => Ok(Self::FOK),
260            TimeInForce::Ioc => Ok(Self::FAK),
261            _ => anyhow::bail!("Unsupported `TimeInForce` for Polymarket: {value:?}"),
262        }
263    }
264}
265
266impl From<PolymarketOrderStatus> for OrderStatus {
267    fn from(value: PolymarketOrderStatus) -> Self {
268        match value {
269            PolymarketOrderStatus::Invalid => Self::Rejected,
270            PolymarketOrderStatus::Live => Self::Accepted,
271            PolymarketOrderStatus::Delayed => Self::Accepted,
272            PolymarketOrderStatus::Matched => Self::Filled,
273            // Placement failure (never became live), treat as rejected
274            PolymarketOrderStatus::Unmatched => Self::Rejected,
275            PolymarketOrderStatus::Canceled => Self::Canceled,
276            // Market resolved = order expired due to market settlement
277            PolymarketOrderStatus::CanceledMarketResolved => Self::Expired,
278        }
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use rstest::rstest;
285
286    use super::*;
287
288    #[rstest]
289    fn test_signature_type_serializes_as_u8() {
290        assert_eq!(serde_json::to_string(&SignatureType::Eoa).unwrap(), "0");
291        assert_eq!(
292            serde_json::to_string(&SignatureType::PolyProxy).unwrap(),
293            "1"
294        );
295        assert_eq!(
296            serde_json::to_string(&SignatureType::PolyGnosisSafe).unwrap(),
297            "2"
298        );
299    }
300
301    #[rstest]
302    fn test_signature_type_deserializes_from_u8() {
303        assert_eq!(
304            serde_json::from_str::<SignatureType>("0").unwrap(),
305            SignatureType::Eoa
306        );
307        assert_eq!(
308            serde_json::from_str::<SignatureType>("1").unwrap(),
309            SignatureType::PolyProxy
310        );
311        assert_eq!(
312            serde_json::from_str::<SignatureType>("2").unwrap(),
313            SignatureType::PolyGnosisSafe
314        );
315    }
316
317    #[rstest]
318    fn test_order_side_serde_screaming_snake() {
319        assert_eq!(
320            serde_json::to_string(&PolymarketOrderSide::Buy).unwrap(),
321            "\"BUY\""
322        );
323        assert_eq!(
324            serde_json::from_str::<PolymarketOrderSide>("\"SELL\"").unwrap(),
325            PolymarketOrderSide::Sell
326        );
327    }
328
329    #[rstest]
330    fn test_event_type_serde_screaming_snake() {
331        assert_eq!(
332            serde_json::to_string(&PolymarketEventType::Placement).unwrap(),
333            "\"PLACEMENT\""
334        );
335        assert_eq!(
336            serde_json::from_str::<PolymarketEventType>("\"TRADE\"").unwrap(),
337            PolymarketEventType::Trade
338        );
339    }
340
341    #[rstest]
342    fn test_order_status_serde_screaming_snake() {
343        assert_eq!(
344            serde_json::to_string(&PolymarketOrderStatus::Live).unwrap(),
345            "\"LIVE\""
346        );
347        assert_eq!(
348            serde_json::from_str::<PolymarketOrderStatus>("\"CANCELED_MARKET_RESOLVED\"").unwrap(),
349            PolymarketOrderStatus::CanceledMarketResolved
350        );
351    }
352
353    #[rstest]
354    fn test_trade_status_serde_screaming_snake() {
355        assert_eq!(
356            serde_json::to_string(&PolymarketTradeStatus::Confirmed).unwrap(),
357            "\"CONFIRMED\""
358        );
359        assert_eq!(
360            serde_json::from_str::<PolymarketTradeStatus>("\"RETRYING\"").unwrap(),
361            PolymarketTradeStatus::Retrying
362        );
363    }
364
365    #[rstest]
366    #[case(PolymarketOrderSide::Buy, OrderSide::Buy)]
367    #[case(PolymarketOrderSide::Sell, OrderSide::Sell)]
368    fn test_order_side_to_nautilus(#[case] poly: PolymarketOrderSide, #[case] expected: OrderSide) {
369        assert_eq!(OrderSide::from(poly), expected);
370    }
371
372    #[rstest]
373    #[case(OrderSide::Buy, PolymarketOrderSide::Buy)]
374    #[case(OrderSide::Sell, PolymarketOrderSide::Sell)]
375    fn test_nautilus_order_side_to_poly(
376        #[case] nautilus: OrderSide,
377        #[case] expected: PolymarketOrderSide,
378    ) {
379        assert_eq!(PolymarketOrderSide::try_from(nautilus).unwrap(), expected);
380    }
381
382    #[rstest]
383    #[case(PolymarketOrderSide::Buy, AggressorSide::Buyer)]
384    #[case(PolymarketOrderSide::Sell, AggressorSide::Seller)]
385    fn test_order_side_to_aggressor(
386        #[case] poly: PolymarketOrderSide,
387        #[case] expected: AggressorSide,
388    ) {
389        assert_eq!(AggressorSide::from(poly), expected);
390    }
391
392    #[rstest]
393    #[case(PolymarketOrderType::GTC, TimeInForce::Gtc)]
394    #[case(PolymarketOrderType::GTD, TimeInForce::Gtd)]
395    #[case(PolymarketOrderType::FOK, TimeInForce::Fok)]
396    #[case(PolymarketOrderType::FAK, TimeInForce::Ioc)]
397    fn test_order_type_to_time_in_force(
398        #[case] poly: PolymarketOrderType,
399        #[case] expected: TimeInForce,
400    ) {
401        assert_eq!(TimeInForce::from(poly), expected);
402    }
403
404    #[rstest]
405    #[case(TimeInForce::Gtc, PolymarketOrderType::GTC)]
406    #[case(TimeInForce::Gtd, PolymarketOrderType::GTD)]
407    #[case(TimeInForce::Fok, PolymarketOrderType::FOK)]
408    #[case(TimeInForce::Ioc, PolymarketOrderType::FAK)]
409    fn test_time_in_force_to_order_type(
410        #[case] tif: TimeInForce,
411        #[case] expected: PolymarketOrderType,
412    ) {
413        assert_eq!(PolymarketOrderType::try_from(tif).unwrap(), expected);
414    }
415
416    #[rstest]
417    #[case(PolymarketOrderStatus::Invalid, OrderStatus::Rejected)]
418    #[case(PolymarketOrderStatus::Live, OrderStatus::Accepted)]
419    #[case(PolymarketOrderStatus::Delayed, OrderStatus::Accepted)]
420    #[case(PolymarketOrderStatus::Matched, OrderStatus::Filled)]
421    #[case(PolymarketOrderStatus::Unmatched, OrderStatus::Rejected)]
422    #[case(PolymarketOrderStatus::Canceled, OrderStatus::Canceled)]
423    #[case(PolymarketOrderStatus::CanceledMarketResolved, OrderStatus::Expired)]
424    fn test_order_status_to_nautilus(
425        #[case] poly: PolymarketOrderStatus,
426        #[case] expected: OrderStatus,
427    ) {
428        assert_eq!(OrderStatus::from(poly), expected);
429    }
430
431    #[rstest]
432    fn test_trade_status_is_finalized() {
433        assert!(PolymarketTradeStatus::Mined.is_finalized());
434        assert!(PolymarketTradeStatus::Confirmed.is_finalized());
435        assert!(!PolymarketTradeStatus::Matched.is_finalized());
436        assert!(!PolymarketTradeStatus::Retrying.is_finalized());
437        assert!(!PolymarketTradeStatus::Failed.is_finalized());
438    }
439}