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