1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19};
20
21use nautilus_core::{
22 from_pydict,
23 python::{IntoPyObjectNautilusExt, to_pyvalue_err},
24};
25use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
26use rust_decimal::Decimal;
27use ustr::Ustr;
28
29use crate::{
30 enums::{AssetClass, InstrumentClass},
31 identifiers::{InstrumentId, Symbol},
32 instruments::BettingInstrument,
33 types::{Currency, Money, Price, Quantity},
34};
35
36#[pymethods]
37#[pyo3_stub_gen::derive::gen_stub_pymethods]
38impl BettingInstrument {
39 #[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
41 #[new]
42 #[pyo3(signature = (instrument_id, raw_symbol, event_type_id, event_type_name, competition_id, competition_name, event_id, event_name, event_country_code, event_open_date, betting_type, market_id, market_name, market_type, market_start_time, selection_id, selection_name, selection_handicap, currency, price_precision, size_precision, price_increment, size_increment, ts_event, ts_init, max_quantity=None, min_quantity=None, max_notional=None, min_notional=None, max_price=None, min_price=None, margin_init=None, margin_maint=None, tick_scheme=None, info=None))]
43 fn py_new(
44 instrument_id: InstrumentId,
45 raw_symbol: Symbol,
46 event_type_id: u64,
47 event_type_name: String,
48 competition_id: u64,
49 competition_name: String,
50 event_id: u64,
51 event_name: String,
52 event_country_code: String,
53 event_open_date: u64,
54 betting_type: String,
55 market_id: String,
56 market_name: String,
57 market_type: String,
58 market_start_time: u64,
59 selection_id: u64,
60 selection_name: String,
61 selection_handicap: f64,
62 currency: Currency,
63 price_precision: u8,
64 size_precision: u8,
65 price_increment: Price,
66 size_increment: Quantity,
67 ts_event: u64,
68 ts_init: u64,
69 max_quantity: Option<Quantity>,
70 min_quantity: Option<Quantity>,
71 max_notional: Option<Money>,
72 min_notional: Option<Money>,
73 max_price: Option<Price>,
74 min_price: Option<Price>,
75 margin_init: Option<Decimal>,
76 margin_maint: Option<Decimal>,
77 tick_scheme: Option<String>,
78 info: Option<Py<PyDict>>,
79 ) -> PyResult<Self> {
80 let info_map = if let Some(info_dict) = info {
82 Python::attach(|py| from_pydict(py, &info_dict))?
83 } else {
84 None
85 };
86
87 Self::builder()
88 .instrument_id(instrument_id)
89 .raw_symbol(raw_symbol)
90 .event_type_id(event_type_id)
91 .event_type_name(Ustr::from(&event_type_name))
92 .competition_id(competition_id)
93 .competition_name(Ustr::from(&competition_name))
94 .event_id(event_id)
95 .event_name(Ustr::from(&event_name))
96 .event_country_code(Ustr::from(&event_country_code))
97 .event_open_date(event_open_date.into())
98 .betting_type(Ustr::from(&betting_type))
99 .market_id(Ustr::from(&market_id))
100 .market_name(Ustr::from(&market_name))
101 .market_type(Ustr::from(&market_type))
102 .market_start_time(market_start_time.into())
103 .selection_id(selection_id)
104 .selection_name(Ustr::from(&selection_name))
105 .selection_handicap(selection_handicap)
106 .currency(currency)
107 .price_precision(price_precision)
108 .size_precision(size_precision)
109 .price_increment(price_increment)
110 .size_increment(size_increment)
111 .maybe_max_quantity(max_quantity)
112 .maybe_min_quantity(min_quantity)
113 .maybe_max_notional(max_notional)
114 .maybe_min_notional(min_notional)
115 .maybe_max_price(max_price)
116 .maybe_min_price(min_price)
117 .maybe_margin_init(margin_init)
118 .maybe_margin_maint(margin_maint)
119 .maybe_tick_scheme(tick_scheme.map(|name| ustr::Ustr::from(name.as_str())))
120 .maybe_info(info_map)
121 .ts_event(ts_event.into())
122 .ts_init(ts_init.into())
123 .build()
124 .map_err(to_pyvalue_err)
125 }
126
127 fn __hash__(&self) -> isize {
128 let mut hasher = DefaultHasher::new();
129 self.hash(&mut hasher);
130 hasher.finish() as isize
131 }
132
133 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
134 match op {
135 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
136 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
137 _ => py.NotImplemented(),
138 }
139 }
140
141 #[getter]
142 fn type_name(&self) -> &'static str {
143 stringify!(BettingInstrument)
144 }
145
146 #[getter]
147 #[pyo3(name = "id")]
148 fn py_id(&self) -> InstrumentId {
149 self.id
150 }
151
152 #[getter]
153 #[pyo3(name = "raw_symbol")]
154 fn py_raw_symbol(&self) -> Symbol {
155 self.raw_symbol
156 }
157
158 #[getter]
159 #[pyo3(name = "asset_class")]
160 fn py_asset_class(&self) -> AssetClass {
161 AssetClass::Alternative
162 }
163
164 #[getter]
165 #[pyo3(name = "instrument_class")]
166 fn py_instrument_class(&self) -> InstrumentClass {
167 InstrumentClass::SportsBetting
168 }
169
170 #[getter]
171 #[pyo3(name = "event_type_id")]
172 fn py_event_type_id(&self) -> u64 {
173 self.event_type_id
174 }
175
176 #[getter]
177 #[pyo3(name = "event_type_name")]
178 fn py_event_type_name(&self) -> &str {
179 self.event_type_name.as_str()
180 }
181
182 #[getter]
183 #[pyo3(name = "competition_id")]
184 fn py_competition_id(&self) -> u64 {
185 self.competition_id
186 }
187
188 #[getter]
189 #[pyo3(name = "competition_name")]
190 fn py_competition_name(&self) -> &str {
191 self.competition_name.as_str()
192 }
193
194 #[getter]
195 #[pyo3(name = "event_id")]
196 fn py_event_id(&self) -> u64 {
197 self.event_id
198 }
199
200 #[getter]
201 #[pyo3(name = "event_name")]
202 fn py_event_name(&self) -> &str {
203 self.event_name.as_str()
204 }
205
206 #[getter]
207 #[pyo3(name = "event_country_code")]
208 fn py_event_country_code(&self) -> &str {
209 self.event_country_code.as_str()
210 }
211
212 #[getter]
213 #[pyo3(name = "event_open_date")]
214 fn py_event_open_date(&self) -> u64 {
215 self.event_open_date.as_u64()
216 }
217
218 #[getter]
219 #[pyo3(name = "betting_type")]
220 fn py_betting_type(&self) -> &str {
221 self.betting_type.as_str()
222 }
223
224 #[getter]
225 #[pyo3(name = "market_id")]
226 fn py_market_id(&self) -> &str {
227 self.market_id.as_str()
228 }
229
230 #[getter]
231 #[pyo3(name = "market_name")]
232 fn py_market_name(&self) -> &str {
233 self.market_name.as_str()
234 }
235
236 #[getter]
237 #[pyo3(name = "market_type")]
238 fn py_market_type(&self) -> &str {
239 self.market_type.as_str()
240 }
241
242 #[getter]
243 #[pyo3(name = "market_start_time")]
244 fn py_market_start_time(&self) -> u64 {
245 self.market_start_time.as_u64()
246 }
247
248 #[getter]
249 #[pyo3(name = "selection_id")]
250 fn py_selection_id(&self) -> u64 {
251 self.selection_id
252 }
253
254 #[getter]
255 #[pyo3(name = "selection_name")]
256 fn py_selection_name(&self) -> &str {
257 self.selection_name.as_str()
258 }
259
260 #[getter]
261 #[pyo3(name = "selection_handicap")]
262 fn py_selection_handicap(&self) -> f64 {
263 self.selection_handicap
264 }
265
266 #[getter]
267 #[pyo3(name = "currency")]
268 fn py_currency(&self) -> Currency {
269 self.currency
270 }
271
272 #[getter]
273 #[pyo3(name = "price_precision")]
274 fn py_price_precision(&self) -> u8 {
275 self.price_precision
276 }
277
278 #[getter]
279 #[pyo3(name = "size_precision")]
280 fn py_size_precision(&self) -> u8 {
281 self.size_precision
282 }
283
284 #[getter]
285 #[pyo3(name = "price_increment")]
286 fn py_price_increment(&self) -> Price {
287 self.price_increment
288 }
289
290 #[getter]
291 #[pyo3(name = "size_increment")]
292 fn py_size_increment(&self) -> Quantity {
293 self.size_increment
294 }
295
296 #[getter]
297 #[pyo3(name = "max_quantity")]
298 fn py_max_quantity(&self) -> Option<Quantity> {
299 self.max_quantity
300 }
301
302 #[getter]
303 #[pyo3(name = "min_quantity")]
304 fn py_min_quantity(&self) -> Option<Quantity> {
305 self.min_quantity
306 }
307
308 #[getter]
309 #[pyo3(name = "max_notional")]
310 fn py_max_notional(&self) -> Option<Money> {
311 self.max_notional
312 }
313
314 #[getter]
315 #[pyo3(name = "min_notional")]
316 fn py_min_notional(&self) -> Option<Money> {
317 self.min_notional
318 }
319
320 #[getter]
321 #[pyo3(name = "max_price")]
322 fn py_max_price(&self) -> Option<Price> {
323 self.max_price
324 }
325
326 #[getter]
327 #[pyo3(name = "min_price")]
328 fn py_min_price(&self) -> Option<Price> {
329 self.min_price
330 }
331
332 #[getter]
333 #[pyo3(name = "info")]
334 fn py_info(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
335 if let Some(ref info_map) = self.info {
337 let py_dict = PyDict::new(py);
338
339 for (key, value) in info_map {
340 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
342 let py_value =
343 PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
344 py_dict.set_item(key, py_value)?;
345 }
346 Ok(py_dict.unbind())
347 } else {
348 Ok(PyDict::new(py).unbind())
349 }
350 }
351
352 #[getter]
353 #[pyo3(name = "ts_event")]
354 fn py_ts_event(&self) -> u64 {
355 self.ts_event.as_u64()
356 }
357
358 #[getter]
359 #[pyo3(name = "ts_init")]
360 fn py_ts_init(&self) -> u64 {
361 self.ts_init.as_u64()
362 }
363
364 #[staticmethod]
365 #[pyo3(name = "from_dict")]
366 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
367 crate::python::instruments::from_dict_instrument_pyo3(py, values)
368 }
369
370 #[pyo3(name = "to_dict")]
371 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
372 let dict = PyDict::new(py);
373 dict.set_item("type", stringify!(BettingInstrument))?;
374 dict.set_item("id", self.id.to_string())?;
375 dict.set_item("raw_symbol", self.raw_symbol.to_string())?;
376 dict.set_item("event_type_id", self.event_type_id)?;
377 dict.set_item("event_type_name", self.event_type_name.to_string())?;
378 dict.set_item("competition_id", self.competition_id)?;
379 dict.set_item("competition_name", self.competition_name.to_string())?;
380 dict.set_item("event_id", self.event_id)?;
381 dict.set_item("event_name", self.event_name.to_string())?;
382 dict.set_item("event_country_code", self.event_country_code.to_string())?;
383 dict.set_item("event_open_date", self.event_open_date.as_u64())?;
384 dict.set_item("betting_type", self.betting_type.to_string())?;
385 dict.set_item("market_id", self.market_id.to_string())?;
386 dict.set_item("market_name", self.market_name.to_string())?;
387 dict.set_item("market_type", self.market_type.to_string())?;
388 dict.set_item("market_start_time", self.market_start_time.as_u64())?;
389 dict.set_item("selection_id", self.selection_id)?;
390 dict.set_item("selection_name", self.selection_name.to_string())?;
391 dict.set_item("selection_handicap", self.selection_handicap)?;
392 dict.set_item("currency", self.currency.code.to_string())?;
393 dict.set_item("price_precision", self.price_precision)?;
394 dict.set_item("size_precision", self.size_precision)?;
395 dict.set_item("price_increment", self.price_increment.to_string())?;
396 dict.set_item("size_increment", self.size_increment.to_string())?;
397 dict.set_item("margin_init", self.margin_init.to_string())?;
398 dict.set_item("margin_maint", self.margin_maint.to_string())?;
399 dict.set_item("ts_event", self.ts_event.as_u64())?;
400 dict.set_item("ts_init", self.ts_init.as_u64())?;
401 if let Some(ref info_map) = self.info {
403 let info_dict = PyDict::new(py);
404
405 for (key, value) in info_map {
406 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
407 let py_value =
408 PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
409 info_dict.set_item(key, py_value)?;
410 }
411 dict.set_item("info", info_dict)?;
412 } else {
413 dict.set_item("info", PyDict::new(py))?;
414 }
415
416 match self.max_quantity {
417 Some(value) => dict.set_item("max_quantity", value.to_string())?,
418 None => dict.set_item("max_quantity", py.None())?,
419 }
420
421 match self.min_quantity {
422 Some(value) => dict.set_item("min_quantity", value.to_string())?,
423 None => dict.set_item("min_quantity", py.None())?,
424 }
425
426 match self.max_notional {
427 Some(value) => dict.set_item("max_notional", value.to_string())?,
428 None => dict.set_item("max_notional", py.None())?,
429 }
430
431 match self.min_notional {
432 Some(value) => dict.set_item("min_notional", value.to_string())?,
433 None => dict.set_item("min_notional", py.None())?,
434 }
435
436 match self.max_price {
437 Some(value) => dict.set_item("max_price", value.to_string())?,
438 None => dict.set_item("max_price", py.None())?,
439 }
440
441 match self.min_price {
442 Some(value) => dict.set_item("min_price", value.to_string())?,
443 None => dict.set_item("min_price", py.None())?,
444 }
445 dict.set_item(
446 "tick_scheme",
447 crate::python::instruments::tick_scheme_to_py(self),
448 )?;
449 Ok(dict.into())
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use pyo3::{prelude::*, types::PyDict};
456 use rstest::rstest;
457
458 use crate::instruments::{BettingInstrument, stubs::*};
459
460 #[rstest]
461 fn test_dict_round_trip(betting: BettingInstrument) {
462 Python::initialize();
463 Python::attach(|py| {
464 let values = betting.py_to_dict(py).unwrap();
465 let values: Py<PyDict> = values.extract(py).unwrap();
466 let new_betting = BettingInstrument::py_from_dict(py, values).unwrap();
467 assert_eq!(betting, new_betting);
468 });
469 }
470}