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