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