nautilus_model/python/instruments/
equity.rs1use 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 identifiers::{InstrumentId, Symbol},
31 instruments::Equity,
32 types::{Currency, Price, Quantity},
33};
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_pymethods]
37impl Equity {
38 #[expect(clippy::too_many_arguments)]
40 #[new]
41 #[pyo3(signature = (instrument_id, raw_symbol, currency, price_precision, price_increment, ts_event, ts_init, isin=None, lot_size=None, max_quantity=None, min_quantity=None, max_price=None, min_price=None, margin_init=None, margin_maint=None, tick_scheme=None, info=None))]
42 fn py_new(
43 instrument_id: InstrumentId,
44 raw_symbol: Symbol,
45 currency: Currency,
46 price_precision: u8,
47 price_increment: Price,
48 ts_event: u64,
49 ts_init: u64,
50 isin: Option<String>,
51 lot_size: Option<Quantity>,
52 max_quantity: Option<Quantity>,
53 min_quantity: Option<Quantity>,
54 max_price: Option<Price>,
55 min_price: Option<Price>,
56 margin_init: Option<Decimal>,
57 margin_maint: Option<Decimal>,
58 tick_scheme: Option<String>,
59 info: Option<Py<PyDict>>,
60 ) -> PyResult<Self> {
61 let info_map = if let Some(info_dict) = info {
63 Python::attach(|py| from_pydict(py, &info_dict))?
64 } else {
65 None
66 };
67
68 Self::builder()
69 .instrument_id(instrument_id)
70 .raw_symbol(raw_symbol)
71 .maybe_isin(isin.map(|x| Ustr::from(&x)))
72 .currency(currency)
73 .price_precision(price_precision)
74 .price_increment(price_increment)
75 .maybe_lot_size(lot_size)
76 .maybe_max_quantity(max_quantity)
77 .maybe_min_quantity(min_quantity)
78 .maybe_max_price(max_price)
79 .maybe_min_price(min_price)
80 .maybe_margin_init(margin_init)
81 .maybe_margin_maint(margin_maint)
82 .maybe_tick_scheme(tick_scheme.map(|name| ustr::Ustr::from(name.as_str())))
83 .maybe_info(info_map)
84 .ts_event(ts_event.into())
85 .ts_init(ts_init.into())
86 .build()
87 .map_err(to_pyvalue_err)
88 }
89
90 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
91 match op {
92 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
93 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
94 _ => py.NotImplemented(),
95 }
96 }
97
98 fn __hash__(&self) -> isize {
99 let mut hasher = DefaultHasher::new();
100 self.hash(&mut hasher);
101 hasher.finish() as isize
102 }
103
104 #[getter]
105 fn type_name(&self) -> &'static str {
106 stringify!(Equity)
107 }
108
109 #[getter]
110 #[pyo3(name = "id")]
111 fn py_id(&self) -> InstrumentId {
112 self.id
113 }
114
115 #[getter]
116 #[pyo3(name = "raw_symbol")]
117 fn py_raw_symbol(&self) -> Symbol {
118 self.raw_symbol
119 }
120
121 #[getter]
122 #[pyo3(name = "isin")]
123 fn py_isin(&self) -> Option<&str> {
124 match self.isin {
125 Some(isin) => Some(isin.as_str()),
126 None => None,
127 }
128 }
129
130 #[getter]
131 #[pyo3(name = "quote_currency")] fn py_quote_currency(&self) -> Currency {
133 self.currency
134 }
135
136 #[getter]
137 #[pyo3(name = "price_precision")]
138 fn py_price_precision(&self) -> u8 {
139 self.price_precision
140 }
141
142 #[getter]
143 #[pyo3(name = "size_precision")]
144 fn py_size_precision(&self) -> u8 {
145 0
146 }
147
148 #[getter]
149 #[pyo3(name = "price_increment")]
150 fn py_price_increment(&self) -> Price {
151 self.price_increment
152 }
153
154 #[getter]
155 #[pyo3(name = "size_increment")]
156 fn py_size_increment(&self) -> Quantity {
157 Quantity::from(1)
158 }
159
160 #[getter]
161 #[pyo3(name = "lot_size")]
162 fn py_lot_size(&self) -> Option<Quantity> {
163 self.lot_size
164 }
165
166 #[getter]
167 #[pyo3(name = "max_quantity")]
168 fn py_max_quantity(&self) -> Option<Quantity> {
169 self.max_quantity
170 }
171
172 #[getter]
173 #[pyo3(name = "min_quantity")]
174 fn py_min_quantity(&self) -> Option<Quantity> {
175 self.min_quantity
176 }
177
178 #[getter]
179 #[pyo3(name = "max_price")]
180 fn py_max_price(&self) -> Option<Price> {
181 self.max_price
182 }
183
184 #[getter]
185 #[pyo3(name = "min_price")]
186 fn py_min_price(&self) -> Option<Price> {
187 self.min_price
188 }
189
190 #[getter]
191 #[pyo3(name = "margin_init")]
192 fn py_margin_init(&self) -> Decimal {
193 self.margin_init
194 }
195
196 #[getter]
197 #[pyo3(name = "margin_maint")]
198 fn py_margin_maint(&self) -> Decimal {
199 self.margin_maint
200 }
201
202 #[getter]
203 #[pyo3(name = "ts_event")]
204 fn py_ts_event(&self) -> u64 {
205 self.ts_event.as_u64()
206 }
207
208 #[getter]
209 #[pyo3(name = "ts_init")]
210 fn py_ts_init(&self) -> u64 {
211 self.ts_init.as_u64()
212 }
213
214 #[getter]
215 #[pyo3(name = "info")]
216 fn py_info(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
217 if let Some(ref info_map) = self.info {
219 let py_dict = PyDict::new(py);
220
221 for (key, value) in info_map {
222 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
224 let py_value =
225 PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
226 py_dict.set_item(key, py_value)?;
227 }
228 Ok(py_dict.unbind())
229 } else {
230 Ok(PyDict::new(py).unbind())
231 }
232 }
233
234 #[staticmethod]
235 #[pyo3(name = "from_dict")]
236 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
237 crate::python::instruments::from_dict_instrument_pyo3(py, values)
238 }
239
240 #[pyo3(name = "to_dict")]
241 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
242 let dict = PyDict::new(py);
243 dict.set_item("type", stringify!(Equity))?;
244 dict.set_item("id", self.id.to_string())?;
245 dict.set_item("raw_symbol", self.raw_symbol.to_string())?;
246 dict.set_item("currency", self.currency.code.to_string())?;
247 dict.set_item("price_precision", self.price_precision)?;
248 dict.set_item("price_increment", self.price_increment.to_string())?;
249 dict.set_item("ts_event", self.ts_event.as_u64())?;
250 dict.set_item("ts_init", self.ts_init.as_u64())?;
251 if let Some(ref info_map) = self.info {
253 let info_dict = PyDict::new(py);
254
255 for (key, value) in info_map {
256 let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
257 let py_value =
258 PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
259 info_dict.set_item(key, py_value)?;
260 }
261 dict.set_item("info", info_dict)?;
262 } else {
263 dict.set_item("info", PyDict::new(py))?;
264 }
265 dict.set_item("margin_init", self.margin_init.to_string())?;
266 dict.set_item("margin_maint", self.margin_maint.to_string())?;
267 match &self.isin {
268 Some(value) => dict.set_item("isin", value.to_string())?,
269 None => dict.set_item("isin", py.None())?,
270 }
271
272 match self.lot_size {
273 Some(value) => dict.set_item("lot_size", value.to_string())?,
274 None => dict.set_item("lot_size", py.None())?,
275 }
276
277 match self.max_quantity {
278 Some(value) => dict.set_item("max_quantity", value.to_string())?,
279 None => dict.set_item("max_quantity", py.None())?,
280 }
281
282 match self.min_quantity {
283 Some(value) => dict.set_item("min_quantity", value.to_string())?,
284 None => dict.set_item("min_quantity", py.None())?,
285 }
286
287 match self.max_price {
288 Some(value) => dict.set_item("max_price", value.to_string())?,
289 None => dict.set_item("max_price", py.None())?,
290 }
291
292 match self.min_price {
293 Some(value) => dict.set_item("min_price", value.to_string())?,
294 None => dict.set_item("min_price", py.None())?,
295 }
296 dict.set_item(
297 "tick_scheme",
298 crate::python::instruments::tick_scheme_to_py(self),
299 )?;
300 Ok(dict.into())
301 }
302}