Skip to main content

nautilus_polymarket/python/
config.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
16use nautilus_model::identifiers::{AccountId, InstrumentId, TraderId};
17use pyo3::pymethods;
18
19use crate::{
20    common::enums::SignatureType,
21    config::{
22        PolymarketDataClientConfig, PolymarketExecClientConfig, PolymarketInstrumentProviderConfig,
23        PolymarketUpDownEventSlugConfig,
24    },
25};
26
27const PY_OPTION_U64_MISSING_SENTINEL: u64 = u64::MAX;
28
29fn resolve_optional_u64_arg(value: Option<u64>, default: Option<u64>) -> Option<u64> {
30    match value {
31        Some(PY_OPTION_U64_MISSING_SENTINEL) => default,
32        other => other,
33    }
34}
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl PolymarketUpDownEventSlugConfig {
39    /// Rust-backed event slug builder for Polymarket Up/Down markets.
40    ///
41    /// Up/Down event slugs follow the pattern
42    /// `{asset}-updown-{interval_mins}m-{unix_timestamp}`, where the timestamp is
43    /// aligned to the start of the interval. The builder emits slugs for each
44    /// configured asset and period.
45    #[new]
46    #[pyo3(signature = (assets=None, interval_mins=None, periods=None, start_offset_periods=None))]
47    fn py_new(
48        assets: Option<Vec<String>>,
49        interval_mins: Option<u64>,
50        periods: Option<u64>,
51        start_offset_periods: Option<i64>,
52    ) -> Self {
53        let default = Self::default();
54        Self {
55            assets: assets.unwrap_or(default.assets),
56            interval_mins: interval_mins.unwrap_or(default.interval_mins),
57            periods: periods.unwrap_or(default.periods),
58            start_offset_periods: start_offset_periods.unwrap_or(default.start_offset_periods),
59        }
60    }
61
62    fn __repr__(&self) -> String {
63        format!("{self:?}")
64    }
65
66    fn __str__(&self) -> String {
67        format!("{self:?}")
68    }
69}
70
71#[pymethods]
72#[pyo3_stub_gen::derive::gen_stub_pymethods]
73impl PolymarketInstrumentProviderConfig {
74    /// Configuration for the Polymarket instrument provider.
75    ///
76    /// This mirrors the Python adapter's `instrument_config` layering so scoped
77    /// market bootstrap can migrate naturally to the Rust/pyO3 live path.
78    #[new]
79    #[pyo3(signature = (load_all=None, load_ids=None, filters=None, event_slugs=None, market_slugs=None, event_slug_builder=None, log_warnings=None, use_gamma_markets=None))]
80    #[expect(clippy::too_many_arguments)]
81    fn py_new(
82        load_all: Option<bool>,
83        load_ids: Option<Vec<InstrumentId>>,
84        filters: Option<std::collections::HashMap<String, String>>,
85        event_slugs: Option<Vec<String>>,
86        market_slugs: Option<Vec<String>>,
87        event_slug_builder: Option<PolymarketUpDownEventSlugConfig>,
88        log_warnings: Option<bool>,
89        use_gamma_markets: Option<bool>,
90    ) -> Self {
91        let default = Self::default();
92        Self {
93            load_all: load_all.unwrap_or(default.load_all),
94            load_ids,
95            filters,
96            event_slugs,
97            market_slugs,
98            event_slug_builder,
99            log_warnings: log_warnings.unwrap_or(default.log_warnings),
100            use_gamma_markets: use_gamma_markets.unwrap_or(default.use_gamma_markets),
101        }
102    }
103
104    fn __repr__(&self) -> String {
105        format!("{self:?}")
106    }
107
108    fn __str__(&self) -> String {
109        format!("{self:?}")
110    }
111}
112
113#[pymethods]
114#[pyo3_stub_gen::derive::gen_stub_pymethods]
115impl PolymarketDataClientConfig {
116    /// Configuration for the Polymarket data client.
117    ///
118    /// `filters` and `new_market_filter` hold `Arc<dyn InstrumentFilter>` trait objects
119    /// and are skipped during serialization; they default to empty/`None` and must be
120    /// installed programmatically after deserialization.
121    #[new]
122    #[pyo3(signature = (instrument_config=None, base_url_http=None, base_url_ws=None, base_url_gamma=None, base_url_data_api=None, http_timeout_secs=None, ws_timeout_secs=None, ws_max_subscriptions=None, update_instruments_interval_mins=PY_OPTION_U64_MISSING_SENTINEL, subscribe_new_markets=None, auto_load_missing_instruments=None, auto_load_debounce_ms=None, auto_load_max_retries=None, auto_load_retry_delay_initial_secs=None, auto_load_retry_delay_max_secs=None, new_market_fetch_max_concurrency=None, resolve_poll_enabled=None, resolve_poll_interval_secs=None, resolve_poll_grace_secs=None, resolve_poll_max_wait_secs=None, base_url_rtds=None))]
123    #[expect(clippy::too_many_arguments)]
124    fn py_new(
125        instrument_config: Option<PolymarketInstrumentProviderConfig>,
126        base_url_http: Option<String>,
127        base_url_ws: Option<String>,
128        base_url_gamma: Option<String>,
129        base_url_data_api: Option<String>,
130        http_timeout_secs: Option<u64>,
131        ws_timeout_secs: Option<u64>,
132        ws_max_subscriptions: Option<usize>,
133        update_instruments_interval_mins: Option<u64>,
134        subscribe_new_markets: Option<bool>,
135        auto_load_missing_instruments: Option<bool>,
136        auto_load_debounce_ms: Option<u64>,
137        auto_load_max_retries: Option<u32>,
138        auto_load_retry_delay_initial_secs: Option<f64>,
139        auto_load_retry_delay_max_secs: Option<f64>,
140        new_market_fetch_max_concurrency: Option<usize>,
141        resolve_poll_enabled: Option<bool>,
142        resolve_poll_interval_secs: Option<u64>,
143        resolve_poll_grace_secs: Option<u64>,
144        resolve_poll_max_wait_secs: Option<u64>,
145        base_url_rtds: Option<String>,
146    ) -> Self {
147        let default = Self::default();
148
149        Self {
150            instrument_config,
151            base_url_http,
152            base_url_ws,
153            base_url_rtds,
154            base_url_gamma,
155            base_url_data_api,
156            http_timeout_secs: http_timeout_secs.unwrap_or(default.http_timeout_secs),
157            ws_timeout_secs: ws_timeout_secs.unwrap_or(default.ws_timeout_secs),
158            ws_max_subscriptions: ws_max_subscriptions.unwrap_or(default.ws_max_subscriptions),
159            update_instruments_interval_mins: resolve_optional_u64_arg(
160                update_instruments_interval_mins,
161                default.update_instruments_interval_mins,
162            ),
163            subscribe_new_markets: subscribe_new_markets.unwrap_or(default.subscribe_new_markets),
164            new_market_fetch_max_concurrency: new_market_fetch_max_concurrency
165                .unwrap_or(default.new_market_fetch_max_concurrency),
166            auto_load_missing_instruments: auto_load_missing_instruments
167                .unwrap_or(default.auto_load_missing_instruments),
168            auto_load_debounce_ms: auto_load_debounce_ms.unwrap_or(default.auto_load_debounce_ms),
169            auto_load_max_retries: auto_load_max_retries.unwrap_or(default.auto_load_max_retries),
170            auto_load_retry_delay_initial_secs: auto_load_retry_delay_initial_secs
171                .unwrap_or(default.auto_load_retry_delay_initial_secs),
172            auto_load_retry_delay_max_secs: auto_load_retry_delay_max_secs
173                .unwrap_or(default.auto_load_retry_delay_max_secs),
174            resolve_poll_enabled: resolve_poll_enabled.unwrap_or(default.resolve_poll_enabled),
175            resolve_poll_interval_secs: resolve_poll_interval_secs
176                .unwrap_or(default.resolve_poll_interval_secs),
177            resolve_poll_grace_secs: resolve_poll_grace_secs
178                .unwrap_or(default.resolve_poll_grace_secs),
179            resolve_poll_max_wait_secs: resolve_poll_max_wait_secs
180                .unwrap_or(default.resolve_poll_max_wait_secs),
181            filters: Vec::new(),
182            new_market_filter: None,
183            transport_backend: default.transport_backend,
184        }
185    }
186
187    fn __repr__(&self) -> String {
188        format!("{self:?}")
189    }
190
191    fn __str__(&self) -> String {
192        format!("{self:?}")
193    }
194}
195
196#[pymethods]
197#[pyo3_stub_gen::derive::gen_stub_pymethods]
198impl PolymarketExecClientConfig {
199    /// Configuration for the Polymarket execution client.
200    ///
201    /// `Debug` is implemented manually to redact secrets, so it is not part of the
202    /// derive list.
203    #[new]
204    #[expect(clippy::too_many_arguments)]
205    #[pyo3(signature = (trader_id=None, account_id=None, private_key=None, api_key=None, api_secret=None, passphrase=None, funder=None, signature_type=None, base_url_http=None, base_url_ws=None, base_url_data_api=None, http_timeout_secs=None, max_retries=None, retry_delay_initial_ms=None, retry_delay_max_ms=None, ack_timeout_secs=None))]
206    fn py_new(
207        trader_id: Option<String>,
208        account_id: Option<String>,
209        private_key: Option<String>,
210        api_key: Option<String>,
211        api_secret: Option<String>,
212        passphrase: Option<String>,
213        funder: Option<String>,
214        signature_type: Option<SignatureType>,
215        base_url_http: Option<String>,
216        base_url_ws: Option<String>,
217        base_url_data_api: Option<String>,
218        http_timeout_secs: Option<u64>,
219        max_retries: Option<u32>,
220        retry_delay_initial_ms: Option<u64>,
221        retry_delay_max_ms: Option<u64>,
222        ack_timeout_secs: Option<u64>,
223    ) -> Self {
224        let default = Self::default();
225        Self {
226            trader_id: trader_id.map_or(default.trader_id, |s| TraderId::from(s.as_str())),
227            account_id: account_id.map_or(default.account_id, |s| AccountId::from(s.as_str())),
228            private_key,
229            api_key,
230            api_secret,
231            passphrase,
232            funder,
233            signature_type: signature_type.unwrap_or(default.signature_type),
234            base_url_http,
235            base_url_ws,
236            base_url_data_api,
237            http_timeout_secs: http_timeout_secs.unwrap_or(default.http_timeout_secs),
238            max_retries: max_retries.unwrap_or(default.max_retries),
239            retry_delay_initial_ms: retry_delay_initial_ms
240                .unwrap_or(default.retry_delay_initial_ms),
241            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(default.retry_delay_max_ms),
242            ack_timeout_secs: ack_timeout_secs.unwrap_or(default.ack_timeout_secs),
243            transport_backend: default.transport_backend,
244        }
245    }
246
247    fn __repr__(&self) -> String {
248        format!("{self:?}")
249    }
250
251    fn __str__(&self) -> String {
252        format!("{self:?}")
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use pyo3::{
259        Bound, IntoPyObject, Python,
260        types::{PyAnyMethods, PyDict, PyDictMethods, PyTuple},
261    };
262    use rstest::rstest;
263
264    use super::*;
265
266    fn construct_data_client_config(
267        py: Python<'_>,
268        args: Option<&Bound<'_, PyTuple>>,
269        kwargs: Option<&Bound<'_, PyDict>>,
270    ) -> PolymarketDataClientConfig {
271        let cls = py.get_type::<PolymarketDataClientConfig>();
272
273        let config = match args {
274            Some(args) => cls.call(args, kwargs),
275            None => cls.call((), kwargs),
276        }
277        .expect("construct PolymarketDataClientConfig");
278
279        config
280            .extract::<PolymarketDataClientConfig>()
281            .expect("extract PolymarketDataClientConfig")
282    }
283
284    #[rstest]
285    fn direct_pyo3_constructor_preserves_none_update_interval() {
286        Python::initialize();
287        Python::attach(|py| {
288            let kwargs = PyDict::new(py);
289            kwargs
290                .set_item("update_instruments_interval_mins", py.None())
291                .unwrap();
292
293            let config = construct_data_client_config(py, None, Some(&kwargs));
294
295            assert_eq!(config.update_instruments_interval_mins, None);
296        });
297    }
298
299    #[rstest]
300    fn direct_pyo3_constructor_uses_default_update_interval_when_omitted() {
301        Python::initialize();
302        Python::attach(|py| {
303            let config = construct_data_client_config(py, None, None);
304
305            assert_eq!(
306                config.update_instruments_interval_mins,
307                PolymarketDataClientConfig::default().update_instruments_interval_mins,
308            );
309        });
310    }
311
312    #[rstest]
313    fn direct_pyo3_constructor_preserves_none_update_interval_for_positional_args() {
314        Python::initialize();
315        Python::attach(|py| {
316            let args = PyTuple::new(
317                py,
318                [
319                    py.None(),
320                    py.None(),
321                    py.None(),
322                    py.None(),
323                    py.None(),
324                    py.None(),
325                    py.None(),
326                    py.None(),
327                    py.None(),
328                    py.None(),
329                ],
330            )
331            .expect("args");
332
333            let config = construct_data_client_config(py, Some(&args), None);
334
335            assert_eq!(config.update_instruments_interval_mins, None);
336        });
337    }
338
339    #[rstest]
340    fn direct_pyo3_constructor_sets_new_market_fetch_max_concurrency() {
341        Python::initialize();
342        Python::attach(|py| {
343            let kwargs = PyDict::new(py);
344            kwargs
345                .set_item("new_market_fetch_max_concurrency", 23)
346                .unwrap();
347
348            let config = construct_data_client_config(py, None, Some(&kwargs));
349
350            assert_eq!(config.new_market_fetch_max_concurrency, 23);
351        });
352    }
353
354    #[rstest]
355    fn direct_pyo3_constructor_sets_base_url_rtds() {
356        Python::initialize();
357        Python::attach(|py| {
358            let kwargs = PyDict::new(py);
359            kwargs
360                .set_item("base_url_rtds", "wss://ws-live-data.example")
361                .unwrap();
362
363            let config = construct_data_client_config(py, None, Some(&kwargs));
364
365            assert_eq!(
366                config.base_url_rtds.as_deref(),
367                Some("wss://ws-live-data.example")
368            );
369        });
370    }
371
372    #[rstest]
373    fn direct_pyo3_constructor_preserves_existing_positional_order() {
374        Python::initialize();
375        Python::attach(|py| {
376            let args = PyTuple::new(
377                py,
378                [
379                    py.None(),
380                    "https://http.example"
381                        .into_pyobject(py)
382                        .unwrap()
383                        .into_any()
384                        .unbind(),
385                    "wss://ws.example"
386                        .into_pyobject(py)
387                        .unwrap()
388                        .into_any()
389                        .unbind(),
390                    "https://gamma.example"
391                        .into_pyobject(py)
392                        .unwrap()
393                        .into_any()
394                        .unbind(),
395                    "https://data.example"
396                        .into_pyobject(py)
397                        .unwrap()
398                        .into_any()
399                        .unbind(),
400                    41_u64.into_pyobject(py).unwrap().into_any().unbind(),
401                    42_u64.into_pyobject(py).unwrap().into_any().unbind(),
402                    512_usize.into_pyobject(py).unwrap().into_any().unbind(),
403                ],
404            )
405            .expect("args");
406
407            let config = construct_data_client_config(py, Some(&args), None);
408
409            assert_eq!(
410                config.base_url_http.as_deref(),
411                Some("https://http.example")
412            );
413            assert_eq!(config.base_url_ws.as_deref(), Some("wss://ws.example"));
414            assert_eq!(
415                config.base_url_gamma.as_deref(),
416                Some("https://gamma.example")
417            );
418            assert_eq!(
419                config.base_url_data_api.as_deref(),
420                Some("https://data.example")
421            );
422            assert_eq!(config.base_url_rtds, None);
423            assert_eq!(config.http_timeout_secs, 41);
424            assert_eq!(config.ws_timeout_secs, 42);
425            assert_eq!(config.ws_max_subscriptions, 512);
426        });
427    }
428
429    #[rstest]
430    fn direct_pyo3_constructor_sets_base_url_rtds_positionally_at_end() {
431        Python::initialize();
432        Python::attach(|py| {
433            let args = PyTuple::new(
434                py,
435                [
436                    py.None(),
437                    py.None(),
438                    py.None(),
439                    py.None(),
440                    py.None(),
441                    py.None(),
442                    py.None(),
443                    py.None(),
444                    py.None(),
445                    py.None(),
446                    py.None(),
447                    py.None(),
448                    py.None(),
449                    py.None(),
450                    py.None(),
451                    py.None(),
452                    py.None(),
453                    py.None(),
454                    py.None(),
455                    py.None(),
456                    "wss://ws-live-data.example"
457                        .into_pyobject(py)
458                        .unwrap()
459                        .into_any()
460                        .unbind(),
461                ],
462            )
463            .expect("args");
464
465            let config = construct_data_client_config(py, Some(&args), None);
466
467            assert_eq!(
468                config.base_url_rtds.as_deref(),
469                Some("wss://ws-live-data.example")
470            );
471        });
472    }
473}