nautilus_model/python/instruments/
crypto_option.rs1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19};
20
21use nautilus_core::python::{
22 IntoPyObjectNautilusExt, serialization::from_dict_pyo3, to_pyvalue_err,
23};
24use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
25use rust_decimal::Decimal;
26
27use crate::{
28 enums::OptionKind,
29 identifiers::{InstrumentId, Symbol},
30 instruments::CryptoOption,
31 types::{Currency, Money, Price, Quantity},
32};
33
34#[pymethods]
35impl CryptoOption {
36 #[allow(clippy::too_many_arguments)]
37 #[new]
38 #[pyo3(signature = (instrument_id, raw_symbol, underlying, quote_currency, settlement_currency, is_inverse, option_kind, strike_price, activation_ns, expiration_ns, price_precision, size_precision, price_increment, size_increment,ts_event, ts_init, multiplier=None, lot_size=None, 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))]
39 fn py_new(
40 instrument_id: InstrumentId,
41 raw_symbol: Symbol,
42 underlying: Currency,
43 quote_currency: Currency,
44 settlement_currency: Currency,
45 is_inverse: bool,
46 option_kind: OptionKind,
47 strike_price: Price,
48 activation_ns: u64,
49 expiration_ns: u64,
50 price_precision: u8,
51 size_precision: u8,
52 price_increment: Price,
53 size_increment: Quantity,
54 ts_event: u64,
55 ts_init: u64,
56 multiplier: Option<Quantity>,
57 lot_size: Option<Quantity>,
58 max_quantity: Option<Quantity>,
59 min_quantity: Option<Quantity>,
60 max_notional: Option<Money>,
61 min_notional: Option<Money>,
62 max_price: Option<Price>,
63 min_price: Option<Price>,
64 margin_init: Option<Decimal>,
65 margin_maint: Option<Decimal>,
66 maker_fee: Option<Decimal>,
67 taker_fee: Option<Decimal>,
68 ) -> PyResult<Self> {
69 Self::new_checked(
70 instrument_id,
71 raw_symbol,
72 underlying,
73 quote_currency,
74 settlement_currency,
75 is_inverse,
76 option_kind,
77 strike_price,
78 activation_ns.into(),
79 expiration_ns.into(),
80 price_precision,
81 size_precision,
82 price_increment,
83 size_increment,
84 multiplier,
85 lot_size,
86 max_quantity,
87 min_quantity,
88 max_notional,
89 min_notional,
90 max_price,
91 min_price,
92 margin_init,
93 margin_maint,
94 maker_fee,
95 taker_fee,
96 ts_event.into(),
97 ts_init.into(),
98 )
99 .map_err(to_pyvalue_err)
100 }
101
102 fn __hash__(&self) -> isize {
103 let mut hasher = DefaultHasher::new();
104 self.hash(&mut hasher);
105 hasher.finish() as isize
106 }
107
108 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
109 match op {
110 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
111 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
112 _ => py.NotImplemented(),
113 }
114 }
115
116 #[getter]
117 fn type_str(&self) -> &str {
118 stringify!(CryptoOption)
119 }
120
121 #[getter]
122 #[pyo3(name = "id")]
123 fn py_id(&self) -> InstrumentId {
124 self.id
125 }
126
127 #[getter]
128 #[pyo3(name = "raw_symbol")]
129 fn py_raw_symbol(&self) -> Symbol {
130 self.raw_symbol
131 }
132
133 #[getter]
134 #[pyo3(name = "underlying")]
135 fn py_underlying(&self) -> Currency {
136 self.underlying
137 }
138
139 #[getter]
140 #[pyo3(name = "quote_currency")]
141 fn py_quote_currency(&self) -> Currency {
142 self.quote_currency
143 }
144
145 #[getter]
146 #[pyo3(name = "settlement_currency")]
147 fn py_settlement_currency(&self) -> Currency {
148 self.settlement_currency
149 }
150
151 #[getter]
152 #[pyo3(name = "is_inverse")]
153 fn py_is_inverse(&self) -> bool {
154 self.is_inverse
155 }
156
157 #[getter]
158 #[pyo3(name = "option_kind")]
159 fn py_option_kind(&self) -> OptionKind {
160 self.option_kind
161 }
162
163 #[getter]
164 #[pyo3(name = "strike_price")]
165 fn py_strike_price(&self) -> Price {
166 self.strike_price
167 }
168
169 #[getter]
170 #[pyo3(name = "activation_ns")]
171 fn py_activation_ns(&self) -> u64 {
172 self.activation_ns.as_u64()
173 }
174
175 #[getter]
176 #[pyo3(name = "expiration_ns")]
177 fn py_expiration_ns(&self) -> u64 {
178 self.expiration_ns.as_u64()
179 }
180
181 #[getter]
182 #[pyo3(name = "price_precision")]
183 fn py_price_precision(&self) -> u8 {
184 self.price_precision
185 }
186
187 #[getter]
188 #[pyo3(name = "size_precision")]
189 fn py_size_precision(&self) -> u8 {
190 self.size_precision
191 }
192
193 #[getter]
194 #[pyo3(name = "price_increment")]
195 fn py_price_increment(&self) -> Price {
196 self.price_increment
197 }
198
199 #[getter]
200 #[pyo3(name = "size_increment")]
201 fn py_size_increment(&self) -> Quantity {
202 self.size_increment
203 }
204
205 #[getter]
206 #[pyo3(name = "multiplier")]
207 fn py_multiplier(&self) -> Quantity {
208 self.multiplier
209 }
210
211 #[getter]
212 #[pyo3(name = "lot_size")]
213 fn py_lot_size(&self) -> Option<Quantity> {
214 Some(self.lot_size)
215 }
216
217 #[getter]
218 #[pyo3(name = "max_quantity")]
219 fn py_max_quantity(&self) -> Option<Quantity> {
220 self.max_quantity
221 }
222
223 #[getter]
224 #[pyo3(name = "min_quantity")]
225 fn py_min_quantity(&self) -> Option<Quantity> {
226 self.min_quantity
227 }
228
229 #[getter]
230 #[pyo3(name = "max_notional")]
231 fn py_max_notional(&self) -> Option<Money> {
232 self.max_notional
233 }
234
235 #[getter]
236 #[pyo3(name = "min_notional")]
237 fn py_min_notional(&self) -> Option<Money> {
238 self.min_notional
239 }
240
241 #[getter]
242 #[pyo3(name = "max_price")]
243 fn py_max_price(&self) -> Option<Price> {
244 self.max_price
245 }
246
247 #[getter]
248 #[pyo3(name = "min_price")]
249 fn py_min_price(&self) -> Option<Price> {
250 self.min_price
251 }
252
253 #[getter]
254 #[pyo3(name = "margin_init")]
255 fn py_margin_init(&self) -> Decimal {
256 self.margin_init
257 }
258
259 #[getter]
260 #[pyo3(name = "margin_maint")]
261 fn py_margin_maint(&self) -> Decimal {
262 self.margin_maint
263 }
264
265 #[getter]
266 #[pyo3(name = "maker_fee")]
267 fn py_maker_fee(&self) -> Decimal {
268 self.maker_fee
269 }
270
271 #[getter]
272 #[pyo3(name = "taker_fee")]
273 fn py_taker_fee(&self) -> Decimal {
274 self.taker_fee
275 }
276
277 #[getter]
278 #[pyo3(name = "info")]
279 fn py_info(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
280 Ok(PyDict::new(py).into())
281 }
282
283 #[getter]
284 #[pyo3(name = "ts_event")]
285 fn py_ts_event(&self) -> u64 {
286 self.ts_event.as_u64()
287 }
288
289 #[getter]
290 #[pyo3(name = "ts_init")]
291 fn py_ts_init(&self) -> u64 {
292 self.ts_init.as_u64()
293 }
294
295 #[staticmethod]
296 #[pyo3(name = "from_dict")]
297 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
298 from_dict_pyo3(py, values)
299 }
300
301 #[pyo3(name = "to_dict")]
302 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
303 let dict = PyDict::new(py);
304 dict.set_item("type", stringify!(CryptoOption))?;
305 dict.set_item("id", self.id.to_string())?;
306 dict.set_item("raw_symbol", self.raw_symbol.to_string())?;
307 dict.set_item("underlying", self.underlying.code.to_string())?;
308 dict.set_item("quote_currency", self.quote_currency.code.to_string())?;
309 dict.set_item(
310 "settlement_currency",
311 self.settlement_currency.code.to_string(),
312 )?;
313 dict.set_item("is_inverse", self.is_inverse)?;
314 dict.set_item("option_kind", self.option_kind.to_string())?;
315 dict.set_item("strike_price", self.strike_price.to_string())?;
316 dict.set_item("activation_ns", self.activation_ns.as_u64())?;
317 dict.set_item("expiration_ns", self.expiration_ns.as_u64())?;
318 dict.set_item("price_precision", self.price_precision)?;
319 dict.set_item("size_precision", self.size_precision)?;
320 dict.set_item("price_increment", self.price_increment.to_string())?;
321 dict.set_item("size_increment", self.size_increment.to_string())?;
322 dict.set_item("multiplier", self.multiplier.to_string())?;
323 dict.set_item("lot_size", self.lot_size.to_string())?;
324 dict.set_item("margin_init", self.margin_init.to_string())?;
325 dict.set_item("margin_maint", self.margin_maint.to_string())?;
326 dict.set_item("maker_fee", self.maker_fee.to_string())?;
327 dict.set_item("taker_fee", self.taker_fee.to_string())?;
328 dict.set_item("ts_event", self.ts_event.as_u64())?;
329 dict.set_item("ts_init", self.ts_init.as_u64())?;
330 dict.set_item("info", PyDict::new(py))?;
331 match self.max_quantity {
332 Some(value) => dict.set_item("max_quantity", value.to_string())?,
333 None => dict.set_item("max_quantity", py.None())?,
334 }
335 match self.min_quantity {
336 Some(value) => dict.set_item("min_quantity", value.to_string())?,
337 None => dict.set_item("min_quantity", py.None())?,
338 }
339 match self.max_notional {
340 Some(value) => dict.set_item("max_notional", value.to_string())?,
341 None => dict.set_item("max_notional", py.None())?,
342 }
343 match self.min_notional {
344 Some(value) => dict.set_item("min_notional", value.to_string())?,
345 None => dict.set_item("min_notional", py.None())?,
346 }
347 match self.max_price {
348 Some(value) => dict.set_item("max_price", value.to_string())?,
349 None => dict.set_item("max_price", py.None())?,
350 }
351 match self.min_price {
352 Some(value) => dict.set_item("min_price", value.to_string())?,
353 None => dict.set_item("min_price", py.None())?,
354 }
355 Ok(dict.into())
356 }
357}
358
359#[cfg(test)]
363mod tests {
364 use pyo3::{prelude::*, types::PyDict};
365 use rstest::rstest;
366
367 use crate::instruments::{CryptoOption, stubs::*};
368
369 #[rstest]
370 fn test_dict_round_trip(crypto_option_btc_deribit: CryptoOption) {
371 Python::initialize();
372 Python::attach(|py| {
373 let crypto_option = crypto_option_btc_deribit;
374 let values = crypto_option.py_to_dict(py).unwrap();
375 let values: Py<PyDict> = values.extract(py).unwrap();
376 let new_crypto_future = CryptoOption::py_from_dict(py, values).unwrap();
377 assert_eq!(crypto_option, new_crypto_future);
378 })
379 }
380}