Skip to main content

nautilus_polymarket/
factories.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//! Factory functions for creating Polymarket clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
19
20use nautilus_common::{
21    cache::Cache,
22    clients::{DataClient, ExecutionClient},
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25};
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::{
28    enums::{AccountType, OmsType},
29    identifiers::ClientId,
30};
31use nautilus_network::retry::RetryConfig;
32
33use crate::{
34    common::consts::POLYMARKET_VENUE,
35    config::{PolymarketDataClientConfig, PolymarketExecClientConfig},
36    data::PolymarketDataClient,
37    execution::PolymarketExecutionClient,
38    http::{
39        clob::PolymarketClobPublicClient, data_api::PolymarketDataApiHttpClient,
40        gamma::PolymarketGammaHttpClient,
41    },
42    websocket::client::PolymarketWebSocketClient,
43};
44
45impl ClientConfig for PolymarketDataClientConfig {
46    fn as_any(&self) -> &dyn Any {
47        self
48    }
49}
50
51/// Factory for creating Polymarket data clients.
52#[cfg_attr(
53    feature = "python",
54    pyo3::pyclass(
55        module = "nautilus_trader.core.nautilus_pyo3.polymarket",
56        from_py_object
57    )
58)]
59#[derive(Debug, Clone)]
60pub struct PolymarketDataClientFactory;
61
62impl DataClientFactory for PolymarketDataClientFactory {
63    fn create(
64        &self,
65        name: &str,
66        config: &dyn ClientConfig,
67        _cache: Rc<RefCell<Cache>>,
68        _clock: Rc<RefCell<dyn Clock>>,
69    ) -> anyhow::Result<Box<dyn DataClient>> {
70        let polymarket_config = config
71            .as_any()
72            .downcast_ref::<PolymarketDataClientConfig>()
73            .ok_or_else(|| {
74                anyhow::anyhow!(
75                    "Invalid config type for PolymarketDataClientFactory. Expected PolymarketDataClientConfig, was {config:?}",
76                )
77            })?
78            .clone();
79
80        let client_id = ClientId::from(name);
81
82        let gamma_client = PolymarketGammaHttpClient::new(
83            Some(polymarket_config.gamma_url()),
84            polymarket_config.http_timeout_secs,
85            RetryConfig {
86                max_retries: 10,
87                initial_delay_ms: 5_000,
88                max_delay_ms: 30_000,
89                backoff_factor: 1.5,
90                jitter_ms: 2_000,
91                operation_timeout_ms: Some(30_000),
92                immediate_first: true,
93                max_elapsed_ms: Some(300_000),
94            },
95        )?;
96
97        let clob_public_client = PolymarketClobPublicClient::new(
98            polymarket_config.base_url_http.clone(),
99            polymarket_config.http_timeout_secs,
100        )?;
101
102        let data_api_client = PolymarketDataApiHttpClient::new(
103            Some(polymarket_config.data_api_url()),
104            polymarket_config.http_timeout_secs,
105        )?;
106
107        let ws_client = PolymarketWebSocketClient::new_market(
108            polymarket_config.base_url_ws.clone(),
109            polymarket_config.subscribe_new_markets,
110            polymarket_config.transport_backend,
111        );
112
113        let mut client = PolymarketDataClient::new(
114            client_id,
115            polymarket_config.clone(),
116            gamma_client,
117            clob_public_client,
118            data_api_client,
119            ws_client,
120        );
121
122        for filter in &polymarket_config.filters {
123            client.add_instrument_filter(Arc::clone(filter));
124        }
125
126        Ok(Box::new(client))
127    }
128
129    fn name(&self) -> &'static str {
130        "POLYMARKET"
131    }
132
133    fn config_type(&self) -> &'static str {
134        "PolymarketDataClientConfig"
135    }
136}
137
138impl ClientConfig for PolymarketExecClientConfig {
139    fn as_any(&self) -> &dyn Any {
140        self
141    }
142}
143
144/// Factory for creating Polymarket execution clients.
145#[cfg_attr(
146    feature = "python",
147    pyo3::pyclass(
148        module = "nautilus_trader.core.nautilus_pyo3.polymarket",
149        from_py_object
150    )
151)]
152#[derive(Debug, Clone)]
153pub struct PolymarketExecutionClientFactory;
154
155impl ExecutionClientFactory for PolymarketExecutionClientFactory {
156    fn create(
157        &self,
158        name: &str,
159        config: &dyn ClientConfig,
160        cache: Rc<RefCell<Cache>>,
161    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
162        let polymarket_config = config
163            .as_any()
164            .downcast_ref::<PolymarketExecClientConfig>()
165            .ok_or_else(|| {
166                anyhow::anyhow!(
167                    "Invalid config type for PolymarketExecutionClientFactory. Expected PolymarketExecClientConfig, was {config:?}",
168                )
169            })?
170            .clone();
171
172        let oms_type = OmsType::Netting;
173        let account_type = AccountType::Cash;
174
175        let client_id = ClientId::from(name);
176        let core = ExecutionClientCore::new(
177            polymarket_config.trader_id,
178            client_id,
179            *POLYMARKET_VENUE,
180            oms_type,
181            polymarket_config.account_id,
182            account_type,
183            None, // base_currency
184            cache,
185        );
186
187        let client = PolymarketExecutionClient::new(core, polymarket_config)?;
188
189        Ok(Box::new(client))
190    }
191
192    fn name(&self) -> &'static str {
193        "POLYMARKET"
194    }
195
196    fn config_type(&self) -> &'static str {
197        "PolymarketExecClientConfig"
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::{cell::RefCell, rc::Rc};
204
205    use nautilus_common::{
206        cache::Cache,
207        clock::TestClock,
208        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
209    };
210    use rstest::rstest;
211
212    use super::*;
213    use crate::config::{PolymarketDataClientConfig, PolymarketExecClientConfig};
214
215    #[derive(Debug)]
216    struct WrongConfig;
217
218    impl ClientConfig for WrongConfig {
219        fn as_any(&self) -> &dyn std::any::Any {
220            self
221        }
222    }
223
224    #[rstest]
225    fn test_polymarket_data_client_factory_creation() {
226        let factory = PolymarketDataClientFactory;
227        assert_eq!(factory.name(), "POLYMARKET");
228        assert_eq!(factory.config_type(), "PolymarketDataClientConfig");
229    }
230
231    #[rstest]
232    fn test_polymarket_data_client_config_implements_client_config() {
233        let config = PolymarketDataClientConfig::default();
234        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
235        let downcasted = boxed_config
236            .as_any()
237            .downcast_ref::<PolymarketDataClientConfig>();
238        assert!(downcasted.is_some());
239    }
240
241    #[rstest]
242    fn test_polymarket_data_client_factory_rejects_wrong_config_type() {
243        let factory = PolymarketDataClientFactory;
244        let wrong_config = WrongConfig;
245        let cache = Rc::new(RefCell::new(Cache::default()));
246        let clock = Rc::new(RefCell::new(TestClock::new()));
247
248        let result = factory.create("POLYMARKET", &wrong_config, cache, clock);
249        assert!(result.is_err());
250        assert!(
251            result
252                .err()
253                .unwrap()
254                .to_string()
255                .contains("Invalid config type")
256        );
257    }
258
259    #[rstest]
260    fn test_polymarket_execution_client_factory_creation() {
261        let factory = PolymarketExecutionClientFactory;
262        assert_eq!(factory.name(), "POLYMARKET");
263        assert_eq!(factory.config_type(), "PolymarketExecClientConfig");
264    }
265
266    #[rstest]
267    fn test_polymarket_exec_client_config_implements_client_config() {
268        let config = PolymarketExecClientConfig::default();
269        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
270        let downcasted = boxed_config
271            .as_any()
272            .downcast_ref::<PolymarketExecClientConfig>();
273        assert!(downcasted.is_some());
274    }
275
276    #[rstest]
277    fn test_polymarket_execution_client_factory_rejects_wrong_config_type() {
278        let factory = PolymarketExecutionClientFactory;
279        let wrong_config = WrongConfig;
280        let cache = Rc::new(RefCell::new(Cache::default()));
281
282        let result = factory.create("POLYMARKET", &wrong_config, cache);
283        assert!(result.is_err());
284        assert!(
285            result
286                .err()
287                .unwrap()
288                .to_string()
289                .contains("Invalid config type")
290        );
291    }
292}