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