1use std::hash::{Hash, Hasher};
17
18use nautilus_core::{
19 Params, UnixNanos,
20 correctness::{CorrectnessResult, CorrectnessResultExt, FAILED, check_equal_u8},
21};
22use rust_decimal::Decimal;
23use serde::{Deserialize, Serialize};
24use ustr::Ustr;
25
26use super::{Instrument, any::InstrumentAny};
27use crate::{
28 enums::{AssetClass, InstrumentClass, OptionKind},
29 identifiers::{InstrumentId, Symbol},
30 types::{
31 currency::Currency,
32 money::Money,
33 price::{Price, check_positive_price},
34 quantity::{Quantity, check_positive_quantity},
35 },
36};
37
38#[repr(C)]
40#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(
42 feature = "python",
43 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
44)]
45#[cfg_attr(
46 feature = "python",
47 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
48)]
49pub struct BinaryOption {
50 pub id: InstrumentId,
52 pub raw_symbol: Symbol,
54 pub asset_class: AssetClass,
56 pub currency: Currency,
58 pub activation_ns: UnixNanos,
60 pub expiration_ns: UnixNanos,
62 pub price_precision: u8,
64 pub size_precision: u8,
66 pub price_increment: Price,
68 pub size_increment: Quantity,
70 pub margin_init: Decimal,
72 pub margin_maint: Decimal,
74 pub maker_fee: Decimal,
76 pub taker_fee: Decimal,
78 pub outcome: Option<Ustr>,
80 pub description: Option<Ustr>,
82 pub max_quantity: Option<Quantity>,
84 pub min_quantity: Option<Quantity>,
86 pub max_notional: Option<Money>,
88 pub min_notional: Option<Money>,
90 pub max_price: Option<Price>,
92 pub min_price: Option<Price>,
94 pub info: Option<Params>,
96 pub ts_event: UnixNanos,
98 pub ts_init: UnixNanos,
100}
101
102impl BinaryOption {
103 #[expect(clippy::too_many_arguments)]
112 pub fn new_checked(
113 instrument_id: InstrumentId,
114 raw_symbol: Symbol,
115 asset_class: AssetClass,
116 currency: Currency,
117 activation_ns: UnixNanos,
118 expiration_ns: UnixNanos,
119 price_precision: u8,
120 size_precision: u8,
121 price_increment: Price,
122 size_increment: Quantity,
123 outcome: Option<Ustr>,
124 description: Option<Ustr>,
125 max_quantity: Option<Quantity>,
126 min_quantity: Option<Quantity>,
127 max_notional: Option<Money>,
128 min_notional: Option<Money>,
129 max_price: Option<Price>,
130 min_price: Option<Price>,
131 margin_init: Option<Decimal>,
132 margin_maint: Option<Decimal>,
133 maker_fee: Option<Decimal>,
134 taker_fee: Option<Decimal>,
135 info: Option<Params>,
136 ts_event: UnixNanos,
137 ts_init: UnixNanos,
138 ) -> CorrectnessResult<Self> {
139 check_equal_u8(
140 price_precision,
141 price_increment.precision,
142 stringify!(price_precision),
143 stringify!(price_increment.precision),
144 )?;
145 check_equal_u8(
146 size_precision,
147 size_increment.precision,
148 stringify!(size_precision),
149 stringify!(size_increment.precision),
150 )?;
151 check_positive_price(price_increment, stringify!(price_increment))?;
152 check_positive_quantity(size_increment, stringify!(size_increment))?;
153
154 Ok(Self {
155 id: instrument_id,
156 raw_symbol,
157 asset_class,
158 currency,
159 activation_ns,
160 expiration_ns,
161 price_precision,
162 size_precision,
163 price_increment,
164 size_increment,
165 margin_init: margin_init.unwrap_or_default(),
166 margin_maint: margin_maint.unwrap_or_default(),
167 maker_fee: maker_fee.unwrap_or_default(),
168 taker_fee: taker_fee.unwrap_or_default(),
169 outcome,
170 description,
171 max_quantity,
172 min_quantity,
173 max_notional,
174 min_notional,
175 max_price,
176 min_price,
177 info,
178 ts_event,
179 ts_init,
180 })
181 }
182
183 #[expect(clippy::too_many_arguments)]
189 #[must_use]
190 pub fn new(
191 instrument_id: InstrumentId,
192 raw_symbol: Symbol,
193 asset_class: AssetClass,
194 currency: Currency,
195 activation_ns: UnixNanos,
196 expiration_ns: UnixNanos,
197 price_precision: u8,
198 size_precision: u8,
199 price_increment: Price,
200 size_increment: Quantity,
201 outcome: Option<Ustr>,
202 description: Option<Ustr>,
203 max_quantity: Option<Quantity>,
204 min_quantity: Option<Quantity>,
205 max_notional: Option<Money>,
206 min_notional: Option<Money>,
207 max_price: Option<Price>,
208 min_price: Option<Price>,
209 margin_init: Option<Decimal>,
210 margin_maint: Option<Decimal>,
211 maker_fee: Option<Decimal>,
212 taker_fee: Option<Decimal>,
213 info: Option<Params>,
214 ts_event: UnixNanos,
215 ts_init: UnixNanos,
216 ) -> Self {
217 Self::new_checked(
218 instrument_id,
219 raw_symbol,
220 asset_class,
221 currency,
222 activation_ns,
223 expiration_ns,
224 price_precision,
225 size_precision,
226 price_increment,
227 size_increment,
228 outcome,
229 description,
230 max_quantity,
231 min_quantity,
232 max_notional,
233 min_notional,
234 max_price,
235 min_price,
236 margin_init,
237 margin_maint,
238 maker_fee,
239 taker_fee,
240 info,
241 ts_event,
242 ts_init,
243 )
244 .expect_display(FAILED)
245 }
246}
247
248impl PartialEq<Self> for BinaryOption {
249 fn eq(&self, other: &Self) -> bool {
250 self.id == other.id
251 }
252}
253
254impl Eq for BinaryOption {}
255
256impl Hash for BinaryOption {
257 fn hash<H: Hasher>(&self, state: &mut H) {
258 self.id.hash(state);
259 }
260}
261
262impl Instrument for BinaryOption {
263 fn into_any(self) -> InstrumentAny {
264 InstrumentAny::BinaryOption(self)
265 }
266
267 fn id(&self) -> InstrumentId {
268 self.id
269 }
270
271 fn raw_symbol(&self) -> Symbol {
272 self.raw_symbol
273 }
274
275 fn asset_class(&self) -> AssetClass {
276 self.asset_class
277 }
278
279 fn instrument_class(&self) -> InstrumentClass {
280 InstrumentClass::BinaryOption
281 }
282
283 fn underlying(&self) -> Option<Ustr> {
284 None
285 }
286
287 fn base_currency(&self) -> Option<Currency> {
288 None
289 }
290
291 fn quote_currency(&self) -> Currency {
292 self.currency
293 }
294
295 fn settlement_currency(&self) -> Currency {
296 self.currency
297 }
298
299 fn isin(&self) -> Option<Ustr> {
300 None
301 }
302
303 fn exchange(&self) -> Option<Ustr> {
304 None
305 }
306
307 fn option_kind(&self) -> Option<OptionKind> {
308 None
309 }
310
311 fn is_inverse(&self) -> bool {
312 false
313 }
314
315 fn price_precision(&self) -> u8 {
316 self.price_precision
317 }
318
319 fn size_precision(&self) -> u8 {
320 self.size_precision
321 }
322
323 fn price_increment(&self) -> Price {
324 self.price_increment
325 }
326
327 fn size_increment(&self) -> Quantity {
328 self.size_increment
329 }
330
331 fn multiplier(&self) -> Quantity {
332 Quantity::from(1)
333 }
334
335 fn lot_size(&self) -> Option<Quantity> {
336 Some(Quantity::from(1))
337 }
338
339 fn max_quantity(&self) -> Option<Quantity> {
340 self.max_quantity
341 }
342
343 fn min_quantity(&self) -> Option<Quantity> {
344 self.min_quantity
345 }
346
347 fn max_price(&self) -> Option<Price> {
348 self.max_price
349 }
350
351 fn min_price(&self) -> Option<Price> {
352 self.min_price
353 }
354
355 fn ts_event(&self) -> UnixNanos {
356 self.ts_event
357 }
358
359 fn ts_init(&self) -> UnixNanos {
360 self.ts_init
361 }
362
363 fn margin_init(&self) -> Decimal {
364 self.margin_init
365 }
366
367 fn margin_maint(&self) -> Decimal {
368 self.margin_maint
369 }
370
371 fn maker_fee(&self) -> Decimal {
372 self.maker_fee
373 }
374
375 fn taker_fee(&self) -> Decimal {
376 self.taker_fee
377 }
378
379 fn strike_price(&self) -> Option<Price> {
380 None
381 }
382
383 fn activation_ns(&self) -> Option<UnixNanos> {
384 Some(self.activation_ns)
385 }
386
387 fn expiration_ns(&self) -> Option<UnixNanos> {
388 Some(self.expiration_ns)
389 }
390
391 fn max_notional(&self) -> Option<Money> {
392 self.max_notional
393 }
394
395 fn min_notional(&self) -> Option<Money> {
396 self.min_notional
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use rstest::rstest;
403
404 use crate::{
405 enums::{AssetClass, InstrumentClass},
406 identifiers::{InstrumentId, Symbol},
407 instruments::{BinaryOption, Instrument, stubs::*},
408 types::{Currency, Price, Quantity},
409 };
410
411 #[rstest]
412 fn test_trait_accessors(binary_option: BinaryOption) {
413 assert_eq!(binary_option.asset_class(), AssetClass::Alternative);
414 assert_eq!(
415 binary_option.instrument_class(),
416 InstrumentClass::BinaryOption
417 );
418 assert_eq!(binary_option.quote_currency(), Currency::USDC());
419 assert!(!binary_option.is_inverse());
420 assert_eq!(binary_option.price_precision(), 3);
421 assert_eq!(binary_option.size_precision(), 2);
422 assert!(binary_option.activation_ns().is_some());
423 assert!(binary_option.expiration_ns().is_some());
424 }
425
426 #[rstest]
427 fn test_new_checked_price_precision_mismatch() {
428 let result = BinaryOption::new_checked(
429 InstrumentId::from("TEST.POLYMARKET"),
430 Symbol::from("TEST"),
431 AssetClass::Alternative,
432 Currency::USDC(),
433 0.into(),
434 0.into(),
435 4, 2,
437 Price::from("0.001"),
438 Quantity::from("0.01"),
439 None,
440 None,
441 None,
442 None,
443 None,
444 None,
445 None,
446 None,
447 None,
448 None,
449 None,
450 None,
451 None,
452 0.into(),
453 0.into(),
454 );
455 assert!(result.is_err());
456 }
457
458 #[rstest]
459 fn test_serialization_roundtrip(binary_option: BinaryOption) {
460 let json = serde_json::to_string(&binary_option).unwrap();
461 let deserialized: BinaryOption = serde_json::from_str(&json).unwrap();
462 assert_eq!(binary_option, deserialized);
463 }
464}