1use 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 #[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 #[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 #[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 #[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}