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