optionstratlib_core/model/leg/
leg_enum.rs1use crate::error::PositionError;
39use crate::model::leg::future::FuturePosition;
40use crate::model::leg::perpetual::PerpetualPosition;
41use crate::model::leg::spot::SpotPosition;
42use crate::model::leg::traits::LegAble;
43use crate::model::position::Position;
44use crate::model::types::Side;
45use positive::Positive;
46use rust_decimal::Decimal;
47use serde::{Deserialize, Serialize};
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
63#[serde(rename_all = "snake_case")]
64pub enum Leg {
65 Option(Box<Position>),
67
68 Spot(SpotPosition),
70
71 Future(FuturePosition),
73
74 Perpetual(PerpetualPosition),
76}
77
78impl Leg {
79 #[must_use]
81 pub fn option(position: Position) -> Self {
82 Self::Option(Box::new(position))
83 }
84
85 #[must_use]
87 pub fn spot(position: SpotPosition) -> Self {
88 Self::Spot(position)
89 }
90
91 #[must_use]
93 pub fn future(position: FuturePosition) -> Self {
94 Self::Future(position)
95 }
96
97 #[must_use]
99 pub fn perpetual(position: PerpetualPosition) -> Self {
100 Self::Perpetual(position)
101 }
102
103 #[must_use]
105 pub fn is_option(&self) -> bool {
106 matches!(self, Self::Option(_))
107 }
108
109 #[must_use]
111 pub fn is_spot(&self) -> bool {
112 matches!(self, Self::Spot(_))
113 }
114
115 #[must_use]
117 pub fn is_future(&self) -> bool {
118 matches!(self, Self::Future(_))
119 }
120
121 #[must_use]
123 pub fn is_perpetual(&self) -> bool {
124 matches!(self, Self::Perpetual(_))
125 }
126
127 #[must_use]
131 pub fn is_linear(&self) -> bool {
132 !self.is_option()
133 }
134
135 #[must_use]
137 pub fn is_derivative(&self) -> bool {
138 !self.is_spot()
139 }
140
141 #[must_use]
143 pub fn is_margined(&self) -> bool {
144 matches!(self, Self::Future(_) | Self::Perpetual(_))
145 }
146
147 #[must_use]
149 pub fn has_expiration(&self) -> bool {
150 matches!(self, Self::Option(_) | Self::Future(_))
151 }
152
153 #[must_use]
155 pub fn as_option(&self) -> Option<&Position> {
156 match self {
157 Self::Option(pos) => Some(pos),
158 _ => None,
159 }
160 }
161
162 #[must_use]
164 pub fn as_spot(&self) -> Option<&SpotPosition> {
165 match self {
166 Self::Spot(pos) => Some(pos),
167 _ => None,
168 }
169 }
170
171 #[must_use]
173 pub fn as_future(&self) -> Option<&FuturePosition> {
174 match self {
175 Self::Future(pos) => Some(pos),
176 _ => None,
177 }
178 }
179
180 #[must_use]
182 pub fn as_perpetual(&self) -> Option<&PerpetualPosition> {
183 match self {
184 Self::Perpetual(pos) => Some(pos),
185 _ => None,
186 }
187 }
188
189 #[must_use]
191 pub fn as_option_mut(&mut self) -> Option<&mut Position> {
192 match self {
193 Self::Option(pos) => Some(pos),
194 _ => None,
195 }
196 }
197
198 #[must_use]
200 pub fn as_spot_mut(&mut self) -> Option<&mut SpotPosition> {
201 match self {
202 Self::Spot(pos) => Some(pos),
203 _ => None,
204 }
205 }
206
207 #[must_use]
209 pub fn as_future_mut(&mut self) -> Option<&mut FuturePosition> {
210 match self {
211 Self::Future(pos) => Some(pos),
212 _ => None,
213 }
214 }
215
216 #[must_use]
218 pub fn as_perpetual_mut(&mut self) -> Option<&mut PerpetualPosition> {
219 match self {
220 Self::Perpetual(pos) => Some(pos),
221 _ => None,
222 }
223 }
224
225 #[must_use]
227 pub fn leg_type_name(&self) -> &'static str {
228 match self {
229 Self::Option(_) => "Option",
230 Self::Spot(_) => "Spot",
231 Self::Future(_) => "Future",
232 Self::Perpetual(_) => "Perpetual",
233 }
234 }
235}
236
237impl LegAble for Leg {
238 fn get_symbol(&self) -> &str {
239 match self {
240 Self::Option(pos) => &pos.option.underlying_symbol,
241 Self::Spot(pos) => pos.get_symbol(),
242 Self::Future(pos) => pos.get_symbol(),
243 Self::Perpetual(pos) => pos.get_symbol(),
244 }
245 }
246
247 fn get_quantity(&self) -> Positive {
248 match self {
249 Self::Option(pos) => pos.option.quantity,
250 Self::Spot(pos) => pos.get_quantity(),
251 Self::Future(pos) => pos.get_quantity(),
252 Self::Perpetual(pos) => pos.get_quantity(),
253 }
254 }
255
256 fn get_side(&self) -> Side {
257 match self {
258 Self::Option(pos) => pos.option.side,
259 Self::Spot(pos) => pos.get_side(),
260 Self::Future(pos) => pos.get_side(),
261 Self::Perpetual(pos) => pos.get_side(),
262 }
263 }
264
265 fn pnl_at_price(&self, price: Positive) -> Result<Decimal, PositionError> {
266 match self {
267 Self::Option(pos) => pos.pnl_at_expiration(&Some(&price)),
272 Self::Spot(pos) => pos.pnl_at_price(price),
273 Self::Future(pos) => pos.pnl_at_price(price),
274 Self::Perpetual(pos) => pos.pnl_at_price(price),
275 }
276 }
277
278 fn total_cost(&self) -> Result<Positive, PositionError> {
279 match self {
280 Self::Option(pos) => pos.total_cost(),
284 Self::Spot(pos) => pos.total_cost(),
285 Self::Future(pos) => pos.total_cost(),
286 Self::Perpetual(pos) => pos.total_cost(),
287 }
288 }
289
290 fn fees(&self) -> Result<Positive, PositionError> {
291 match self {
292 Self::Option(pos) => Ok(pos.open_fee.checked_add(&pos.close_fee)?),
298 Self::Spot(pos) => pos.fees(),
299 Self::Future(pos) => pos.fees(),
300 Self::Perpetual(pos) => pos.fees(),
301 }
302 }
303
304 fn notional_value(&self, price: Positive) -> Result<Positive, PositionError> {
313 match self {
314 Self::Option(pos) => Ok(pos
315 .option
316 .quantity
317 .checked_mul(&pos.option.contract_size)?
318 .checked_mul(&price)?),
319 Self::Spot(pos) => pos.notional_value(price),
320 Self::Future(pos) => pos.notional_value(price),
321 Self::Perpetual(pos) => pos.notional_value(price),
322 }
323 }
324}
325
326impl std::fmt::Display for Leg {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 match self {
329 Self::Option(pos) => write!(f, "[Option] {}", pos),
330 Self::Spot(pos) => write!(f, "[Spot] {}", pos),
331 Self::Future(pos) => write!(f, "[Future] {}", pos),
332 Self::Perpetual(pos) => write!(f, "[Perpetual] {}", pos),
333 }
334 }
335}
336
337impl From<Position> for Leg {
338 fn from(position: Position) -> Self {
339 Self::Option(Box::new(position))
340 }
341}
342
343impl From<SpotPosition> for Leg {
344 fn from(position: SpotPosition) -> Self {
345 Self::Spot(position)
346 }
347}
348
349impl From<FuturePosition> for Leg {
350 fn from(position: FuturePosition) -> Self {
351 Self::Future(position)
352 }
353}
354
355impl From<PerpetualPosition> for Leg {
356 fn from(position: PerpetualPosition) -> Self {
357 Self::Perpetual(position)
358 }
359}
360
361#[cfg(all(test, feature = "clock"))]
362mod tests {
363 use super::*;
364 use crate::model::ExpirationDate;
365 use crate::model::types::OptionStyle;
366 use crate::model::utils::create_sample_option_simplest;
367
368 use positive::pos_or_panic;
369
370 fn create_test_option_position() -> Position {
371 let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
372 Position::new(
373 option,
374 pos_or_panic!(5.0),
375 chrono::DateTime::UNIX_EPOCH,
376 pos_or_panic!(0.5),
377 pos_or_panic!(0.5),
378 None,
379 None,
380 )
381 }
382
383 fn create_test_spot_position() -> SpotPosition {
384 SpotPosition::long("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0))
385 }
386
387 fn create_test_future_position() -> FuturePosition {
388 FuturePosition::long(
389 "ES".to_string(),
390 Positive::ONE,
391 pos_or_panic!(4500.0),
392 ExpirationDate::Days(pos_or_panic!(30.0)),
393 pos_or_panic!(50.0),
394 pos_or_panic!(15000.0),
395 )
396 }
397
398 fn create_test_perpetual_position() -> PerpetualPosition {
399 PerpetualPosition::long(
400 "BTC-USDT-PERP".to_string(),
401 Positive::ONE,
402 pos_or_panic!(50000.0),
403 pos_or_panic!(10.0),
404 pos_or_panic!(5000.0),
405 )
406 }
407
408 #[test]
409 fn test_leg_option_creation() {
410 let pos = create_test_option_position();
411 let leg = Leg::option(pos.clone());
412
413 assert!(leg.is_option());
414 assert!(!leg.is_spot());
415 assert!(!leg.is_future());
416 assert!(!leg.is_perpetual());
417 assert!(!leg.is_linear());
418 assert!(leg.is_derivative());
419 assert!(leg.has_expiration());
420 }
421
422 #[test]
423 fn test_leg_spot_creation() {
424 let pos = create_test_spot_position();
425 let leg = Leg::spot(pos);
426
427 assert!(!leg.is_option());
428 assert!(leg.is_spot());
429 assert!(!leg.is_future());
430 assert!(!leg.is_perpetual());
431 assert!(leg.is_linear());
432 assert!(!leg.is_derivative());
433 assert!(!leg.has_expiration());
434 assert!(!leg.is_margined());
435 }
436
437 #[test]
438 fn test_leg_future_creation() {
439 let pos = create_test_future_position();
440 let leg = Leg::future(pos);
441
442 assert!(!leg.is_option());
443 assert!(!leg.is_spot());
444 assert!(leg.is_future());
445 assert!(!leg.is_perpetual());
446 assert!(leg.is_linear());
447 assert!(leg.is_derivative());
448 assert!(leg.has_expiration());
449 assert!(leg.is_margined());
450 }
451
452 #[test]
453 fn test_leg_perpetual_creation() {
454 let pos = create_test_perpetual_position();
455 let leg = Leg::perpetual(pos);
456
457 assert!(!leg.is_option());
458 assert!(!leg.is_spot());
459 assert!(!leg.is_future());
460 assert!(leg.is_perpetual());
461 assert!(leg.is_linear());
462 assert!(leg.is_derivative());
463 assert!(!leg.has_expiration());
464 assert!(leg.is_margined());
465 }
466
467 #[test]
468 fn test_leg_from_position() {
469 let pos = create_test_option_position();
470 let leg: Leg = pos.into();
471 assert!(leg.is_option());
472 }
473
474 #[test]
475 fn test_leg_from_spot() {
476 let pos = create_test_spot_position();
477 let leg: Leg = pos.into();
478 assert!(leg.is_spot());
479 }
480
481 #[test]
482 fn test_leg_from_future() {
483 let pos = create_test_future_position();
484 let leg: Leg = pos.into();
485 assert!(leg.is_future());
486 }
487
488 #[test]
489 fn test_leg_from_perpetual() {
490 let pos = create_test_perpetual_position();
491 let leg: Leg = pos.into();
492 assert!(leg.is_perpetual());
493 }
494
495 #[test]
496 fn test_leg_as_option() {
497 let pos = create_test_option_position();
498 let leg = Leg::option(pos);
499
500 assert!(leg.as_option().is_some());
501 assert!(leg.as_spot().is_none());
502 assert!(leg.as_future().is_none());
503 assert!(leg.as_perpetual().is_none());
504 }
505
506 #[test]
507 fn test_leg_as_spot() {
508 let pos = create_test_spot_position();
509 let leg = Leg::spot(pos);
510
511 assert!(leg.as_option().is_none());
512 assert!(leg.as_spot().is_some());
513 assert!(leg.as_future().is_none());
514 assert!(leg.as_perpetual().is_none());
515 }
516
517 #[test]
518 fn test_leg_get_symbol() {
519 let spot = create_test_spot_position();
520 let leg = Leg::spot(spot);
521 assert_eq!(leg.get_symbol(), "AAPL");
522
523 let future = create_test_future_position();
524 let leg = Leg::future(future);
525 assert_eq!(leg.get_symbol(), "ES");
526
527 let perp = create_test_perpetual_position();
528 let leg = Leg::perpetual(perp);
529 assert_eq!(leg.get_symbol(), "BTC-USDT-PERP");
530 }
531
532 #[cfg(feature = "clock")]
533 #[test]
534 fn test_leg_get_quantity() {
535 let spot = SpotPosition::long("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
536 let leg = Leg::spot(spot);
537 assert_eq!(leg.get_quantity(), Positive::HUNDRED);
538 }
539
540 #[cfg(feature = "clock")]
541 #[test]
542 fn test_leg_get_side() {
543 let long_spot =
544 SpotPosition::long("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
545 let leg = Leg::spot(long_spot);
546 assert_eq!(leg.get_side(), Side::Long);
547
548 let short_spot =
549 SpotPosition::short("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
550 let leg = Leg::spot(short_spot);
551 assert_eq!(leg.get_side(), Side::Short);
552 }
553
554 #[cfg(feature = "clock")]
555 #[test]
556 fn test_leg_pnl_spot() {
557 let spot = SpotPosition::long("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
558 let leg = Leg::spot(spot);
559
560 let pnl = leg.pnl_at_price(pos_or_panic!(160.0));
561 assert_eq!(pnl.ok(), Some(Decimal::from(1000)));
562 }
563
564 #[test]
565 fn test_leg_type_name() {
566 let option_leg = Leg::option(create_test_option_position());
567 assert_eq!(option_leg.leg_type_name(), "Option");
568
569 let spot_leg = Leg::spot(create_test_spot_position());
570 assert_eq!(spot_leg.leg_type_name(), "Spot");
571
572 let future_leg = Leg::future(create_test_future_position());
573 assert_eq!(future_leg.leg_type_name(), "Future");
574
575 let perp_leg = Leg::perpetual(create_test_perpetual_position());
576 assert_eq!(perp_leg.leg_type_name(), "Perpetual");
577 }
578
579 #[test]
580 fn test_leg_display() {
581 let spot = create_test_spot_position();
582 let leg = Leg::spot(spot);
583 let display = format!("{}", leg);
584 assert!(display.contains("[Spot]"));
585 assert!(display.contains("AAPL"));
586 }
587
588 #[test]
589 fn test_leg_total_cost() {
590 let spot = SpotPosition::new(
591 "AAPL".to_string(),
592 Positive::HUNDRED,
593 pos_or_panic!(150.0),
594 Side::Long,
595 chrono::DateTime::UNIX_EPOCH,
596 pos_or_panic!(10.0),
597 pos_or_panic!(10.0),
598 );
599 let leg = Leg::spot(spot);
600 assert_eq!(leg.total_cost().ok(), Some(pos_or_panic!(15020.0)));
601 }
602
603 #[test]
604 fn test_leg_fees() {
605 let spot = SpotPosition::new(
606 "AAPL".to_string(),
607 Positive::HUNDRED,
608 pos_or_panic!(150.0),
609 Side::Long,
610 chrono::DateTime::UNIX_EPOCH,
611 pos_or_panic!(10.0),
612 pos_or_panic!(15.0),
613 );
614 let leg = Leg::spot(spot);
615 assert_eq!(leg.fees().ok(), Some(pos_or_panic!(25.0)));
616 }
617
618 #[test]
621 fn test_leg_notional_value_option_overflow_is_error() {
622 let mut position = create_test_option_position();
623 position.option.quantity = Positive::TWO;
624 let leg = Leg::option(position);
625 assert!(leg.notional_value(Positive::MAX).is_err());
626 assert_eq!(
627 leg.notional_value(Positive::HUNDRED).ok(),
628 Some(pos_or_panic!(200.0))
629 );
630 }
631}