1use chrono::{DateTime, Duration, NaiveDate, Utc};
2use enum_dispatch::enum_dispatch;
3use num_rational::Rational64;
4use num_traits::{Signed, ToPrimitive, Zero};
5use ordered_float::NotNan;
6
7use std::convert::TryInto;
8use std::error::Error;
9use std::fmt;
10use std::str::FromStr;
11
12const SHARE_UNIT_DELTA: f64 = 0.01;
13
14#[enum_dispatch]
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub enum Position {
17 OptionsPosition,
18 SharesPosition,
19}
20
21#[enum_dispatch(Position)]
23pub trait GenericPosition {
24 fn symbol(&self) -> &str;
26
27 fn underlying_symbol(&self) -> &str;
30
31 fn is_long(&self) -> bool;
33
34 fn unit_cost(&self) -> Option<Rational64>;
36 fn unit_cost_mut(&mut self) -> &mut Option<Rational64>;
37
38 fn unit_bid_price(&self) -> Option<Rational64>;
40 fn unit_bid_price_mut(&mut self) -> &mut Option<Rational64>;
41
42 fn unit_ask_price(&self) -> Option<Rational64>;
44 fn unit_ask_price_mut(&mut self) -> &mut Option<Rational64>;
45
46 fn unit_delta(&self) -> Option<NotNan<f64>>;
48
49 fn unit_vega(&self) -> Option<NotNan<f64>>;
51
52 fn unit_theta(&self) -> Option<NotNan<f64>>;
54
55 fn quantity(&self) -> Rational64;
57 fn quantity_mut(&mut self) -> &mut Rational64;
58
59 fn signed_quantity(&self) -> Rational64 {
61 if self.is_long() {
62 self.quantity()
63 } else {
64 -self.quantity()
65 }
66 }
67
68 fn cost(&self) -> Option<Rational64> {
70 self.unit_cost().map(|x| x * self.quantity())
71 }
72
73 fn net_liq(&self) -> Option<Rational64> {
74 self.unit_mid_price().map(|x| x * self.quantity())
75 }
76
77 fn bid_price(&self) -> Option<Rational64> {
79 self.unit_bid_price().map(|x| x * self.quantity())
80 }
81
82 fn ask_price(&self) -> Option<Rational64> {
84 self.unit_ask_price().map(|x| x * self.quantity())
85 }
86
87 fn mid_price(&self) -> Option<Rational64> {
89 self.unit_mid_price().map(|x| x * self.quantity())
90 }
91
92 fn unit_mid_price(&self) -> Option<Rational64> {
94 Some((self.unit_bid_price()? + self.unit_ask_price()?) / 2)
95 }
96
97 fn delta(&self) -> Option<NotNan<f64>> {
99 Some(self.unit_delta()? * self.quantity().to_f64()?)
100 }
101
102 fn vega(&self) -> Option<NotNan<f64>> {
104 Some(self.unit_vega()? * self.quantity().to_f64()?)
105 }
106
107 fn theta(&self) -> Option<NotNan<f64>> {
109 Some(self.unit_theta()? * self.quantity().to_f64()?)
110 }
111
112 fn equivalent_strike_price(&self) -> Rational64;
115
116 fn equivalent_option_type(&self) -> OptionType;
118
119 fn equivalent_lot_size(&self) -> i64;
122
123 fn profit_at_expiry(&self, underlying_price: Rational64) -> Rational64 {
124 let lot_size: i64 = self.equivalent_lot_size().try_into().unwrap();
125
126 let unit_expiry_net_liq = match self.equivalent_option_type() {
127 OptionType::Call => underlying_price - self.equivalent_strike_price(),
128 OptionType::Put => self.equivalent_strike_price() - underlying_price,
129 }
130 .max(Rational64::zero())
131 * if self.is_long() { lot_size } else { -lot_size };
132
133 (self.unit_cost().expect("Undefined cost") + unit_expiry_net_liq) * self.quantity()
134 }
135}
136
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct OptionsPosition {
139 pub symbol: String,
141
142 pub underlying_symbol: String,
144
145 pub is_long: bool,
147
148 pub unit_cost: Option<Rational64>,
150
151 pub unit_bid_price: Option<Rational64>,
153
154 pub unit_ask_price: Option<Rational64>,
156
157 pub unit_delta: Option<NotNan<f64>>,
159
160 pub unit_vega: Option<NotNan<f64>>,
162
163 pub unit_theta: Option<NotNan<f64>>,
165
166 pub quantity: Rational64,
168
169 pub strike_price: Rational64,
171
172 pub option_type: OptionType,
174
175 pub expiration_date: ExpirationDate,
177
178 pub lot_size: Option<i64>,
180}
181
182impl OptionsPosition {
183 pub fn description(&self) -> String {
184 format!(
185 "{} {:.2} {:?}",
186 self.expiration_date,
187 self.strike_price.to_f64().unwrap(),
188 self.option_type,
189 )
190 }
191
192 #[cfg(test)]
193 pub fn mock(
194 option_type: OptionType,
195 strike_price: i64,
196 unit_cost: i64,
197 quantity: Rational64,
198 ) -> Position {
199 let is_long = unit_cost < 0;
200 OptionsPosition {
201 symbol: "OPTION".to_string(),
202 underlying_symbol: "ABC".to_string(),
203 option_type,
204 strike_price: Rational64::from_integer(strike_price),
205 expiration_date: Default::default(),
206 is_long,
207 unit_cost: Some(Rational64::from_integer(unit_cost)),
208 unit_bid_price: None,
209 unit_ask_price: None,
210 unit_delta: None,
211 unit_vega: None,
212 unit_theta: None,
213 quantity,
214 lot_size: None,
215 }
216 .into()
217 }
218}
219
220impl GenericPosition for OptionsPosition {
221 fn symbol(&self) -> &str {
222 &self.symbol
223 }
224
225 fn underlying_symbol(&self) -> &str {
226 &self.underlying_symbol
227 }
228
229 fn is_long(&self) -> bool {
230 self.is_long
231 }
232
233 fn unit_cost(&self) -> Option<Rational64> {
234 self.unit_cost
235 }
236
237 fn unit_cost_mut(&mut self) -> &mut Option<Rational64> {
238 &mut self.unit_cost
239 }
240
241 fn unit_bid_price(&self) -> Option<Rational64> {
242 self.unit_bid_price
243 }
244
245 fn unit_bid_price_mut(&mut self) -> &mut Option<Rational64> {
246 &mut self.unit_bid_price
247 }
248
249 fn unit_ask_price(&self) -> Option<Rational64> {
250 self.unit_ask_price
251 }
252
253 fn unit_ask_price_mut(&mut self) -> &mut Option<Rational64> {
254 &mut self.unit_ask_price
255 }
256
257 fn unit_delta(&self) -> Option<NotNan<f64>> {
258 self.unit_delta
259 }
260
261 fn unit_vega(&self) -> Option<NotNan<f64>> {
262 self.unit_vega
263 }
264
265 fn unit_theta(&self) -> Option<NotNan<f64>> {
266 self.unit_theta
267 }
268
269 fn quantity(&self) -> Rational64 {
270 self.quantity
271 }
272
273 fn quantity_mut(&mut self) -> &mut Rational64 {
274 &mut self.quantity
275 }
276
277 fn equivalent_strike_price(&self) -> Rational64 {
278 self.strike_price
279 }
280
281 fn equivalent_option_type(&self) -> OptionType {
282 self.option_type
283 }
284
285 fn equivalent_lot_size(&self) -> i64 {
286 self.lot_size.unwrap_or(100)
287 }
288}
289
290#[derive(Clone, Debug, Eq, PartialEq)]
291pub struct SharesPosition {
292 pub symbol: String,
294
295 pub is_long: bool,
297
298 pub unit_cost: Option<Rational64>,
300
301 pub unit_bid_price: Option<Rational64>,
303
304 pub unit_ask_price: Option<Rational64>,
306
307 pub quantity: Rational64,
309}
310
311impl SharesPosition {
312 #[cfg(test)]
313 pub fn mock(unit_cost: i64, quantity: Rational64) -> Position {
314 let is_long = unit_cost < 0;
315 SharesPosition {
316 symbol: "ABC".to_string(),
317 is_long,
318 unit_cost: Some(Rational64::from_integer(unit_cost)),
319 unit_bid_price: None,
320 unit_ask_price: None,
321 quantity,
322 }
323 .into()
324 }
325}
326
327impl GenericPosition for SharesPosition {
328 fn symbol(&self) -> &str {
329 &self.symbol
330 }
331
332 fn underlying_symbol(&self) -> &str {
333 &self.symbol
334 }
335
336 fn is_long(&self) -> bool {
337 self.is_long
338 }
339
340 fn unit_cost(&self) -> Option<Rational64> {
341 self.unit_cost
342 }
343
344 fn unit_cost_mut(&mut self) -> &mut Option<Rational64> {
345 &mut self.unit_cost
346 }
347
348 fn unit_bid_price(&self) -> Option<Rational64> {
349 self.unit_bid_price
350 }
351
352 fn unit_bid_price_mut(&mut self) -> &mut Option<Rational64> {
353 &mut self.unit_bid_price
354 }
355
356 fn unit_ask_price(&self) -> Option<Rational64> {
357 self.unit_ask_price
358 }
359
360 fn unit_ask_price_mut(&mut self) -> &mut Option<Rational64> {
361 &mut self.unit_ask_price
362 }
363
364 fn unit_delta(&self) -> Option<NotNan<f64>> {
365 Some(
366 NotNan::new(if self.is_long {
367 SHARE_UNIT_DELTA
368 } else {
369 -SHARE_UNIT_DELTA
370 })
371 .unwrap(),
372 )
373 }
374
375 fn unit_vega(&self) -> Option<NotNan<f64>> {
376 Some(NotNan::new(0.0).unwrap())
377 }
378
379 fn unit_theta(&self) -> Option<NotNan<f64>> {
380 Some(NotNan::new(0.0).unwrap())
381 }
382
383 fn quantity(&self) -> Rational64 {
384 self.quantity
385 }
386
387 fn quantity_mut(&mut self) -> &mut Rational64 {
388 &mut self.quantity
389 }
390
391 fn equivalent_strike_price(&self) -> Rational64 {
392 Rational64::zero()
393 }
394
395 fn equivalent_option_type(&self) -> OptionType {
396 OptionType::Call
397 }
398
399 fn equivalent_lot_size(&self) -> i64 {
400 1
401 }
402}
403
404#[derive(Copy, Clone, Debug, Eq, PartialEq)]
405pub struct Decimal(pub Rational64);
406
407impl Decimal {
408 pub fn abs(&self) -> Decimal {
409 Decimal(self.0.abs())
410 }
411}
412
413#[derive(Debug, Clone)]
414struct DecimalFromStrError(String);
415
416impl Error for DecimalFromStrError {}
417
418impl fmt::Display for DecimalFromStrError {
419 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
420 write!(f, "'{}' could not be parsed as decimal", self.0)
421 }
422}
423
424impl FromStr for Decimal {
425 type Err = Box<dyn Error>;
426
427 fn from_str(s: &str) -> Result<Self, Self::Err> {
428 let without_commas = s.trim().replace(',', "");
429 let has_negative_sign = without_commas.starts_with('-');
430 let without_sign = without_commas.replace('-', "").replace('+', "");
431 let decimal_idx = without_sign.chars().position(|c| c == '.');
432 let without_decimal = without_sign.replace('.', "");
433
434 let mut numerator =
435 i64::from_str(&without_decimal).map_err(|_| DecimalFromStrError(s.to_string()))?;
436 if has_negative_sign {
437 numerator *= -1;
438 }
439
440 let denominator = if let Some(decimal_idx) = decimal_idx {
441 10i64.pow(
442 without_decimal
443 .len()
444 .checked_sub(decimal_idx)
445 .and_then(|d| TryInto::<u32>::try_into(d).ok())
446 .ok_or_else(|| DecimalFromStrError(s.to_string()))?,
447 )
448 } else {
449 1
450 };
451 Ok(Decimal(Rational64::new(numerator, denominator)))
452 }
453}
454
455impl fmt::Display for Decimal {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 write!(f, "{}", self.0.to_f64().unwrap())
458 }
459}
460
461impl Default for Decimal {
462 fn default() -> Self {
463 Decimal(Rational64::zero())
464 }
465}
466
467#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
468pub struct ExpirationDate(pub NaiveDate);
469
470impl ExpirationDate {
471 pub fn time_to_expiration(&self, now: Option<fn() -> DateTime<Utc>>) -> Duration {
472 let date_now = now.unwrap_or(Utc::now)().naive_utc().date();
473 self.0 - date_now
474 }
475}
476
477impl FromStr for ExpirationDate {
478 type Err = chrono::ParseError;
479
480 fn from_str(s: &str) -> Result<Self, Self::Err> {
481 Ok(ExpirationDate(NaiveDate::from_str(s)?))
482 }
483}
484
485impl Default for ExpirationDate {
486 fn default() -> Self {
487 ExpirationDate(NaiveDate::from_ymd(1, 1, 1))
488 }
489}
490
491impl fmt::Display for ExpirationDate {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 self.0.fmt(f)
494 }
495}
496
497#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
498pub enum OptionType {
499 Call,
500 Put,
501}
502
503impl Default for OptionType {
504 fn default() -> Self {
505 OptionType::Call
506 }
507}
508
509#[derive(Clone, Debug, Eq, PartialEq)]
510pub struct StrategyBreakevens {
511 pub breakevens: Vec<Breakeven>,
513}
514
515impl StrategyBreakevens {
516 pub fn min(&self) -> Option<&Breakeven> {
517 match (self.breakevens.first(), self.breakevens.len()) {
518 (Some(breakeven), 1) => {
519 if breakeven.is_ascending {
520 Some(breakeven)
521 } else {
522 None
523 }
524 }
525 (breakeven, _) => breakeven,
526 }
527 }
528
529 pub fn max(&self) -> Option<&Breakeven> {
530 match (self.breakevens.last(), self.breakevens.len()) {
531 (Some(breakeven), 1) => {
532 if !breakeven.is_ascending {
533 Some(breakeven)
534 } else {
535 None
536 }
537 }
538 (breakeven, _) => breakeven,
539 }
540 }
541}
542
543#[derive(Clone, Debug, Eq, PartialEq)]
544pub struct Breakeven {
545 pub price: Rational64,
546 pub is_ascending: bool,
548}
549
550pub fn calculate_breakevens_for_strategy(positions: &[Position]) -> StrategyBreakevens {
552 if positions.is_empty() {
553 return StrategyBreakevens { breakevens: vec![] };
554 }
555
556 let max_strike_price = positions
557 .iter()
558 .map(|position| position.equivalent_strike_price())
559 .max()
560 .unwrap();
561
562 let profit_at_price = |price| {
563 let profit: Rational64 = positions
564 .iter()
565 .map(|position| position.profit_at_expiry(price))
566 .sum();
567 profit
568 };
569
570 const MARGIN_SCALE_FACTOR: i64 = 1000;
572 let price_range = (
573 Rational64::zero(),
574 (max_strike_price + 1) * MARGIN_SCALE_FACTOR,
576 );
577
578 let mut prev_price = price_range.0;
579 let mut prev_profit = profit_at_price(prev_price);
580
581 let mut breakevens = vec![];
582
583 for strike_price in positions
584 .iter()
585 .map(|position| position.equivalent_strike_price())
586 .chain(std::iter::once(price_range.1))
587 {
588 if strike_price == prev_price {
589 continue;
590 }
591
592 assert!(
593 strike_price > prev_price,
594 "Options should be sorted by strike price"
595 );
596
597 let profit = profit_at_price(strike_price);
598 if profit.is_negative() != prev_profit.is_negative() {
599 let x = strike_price - prev_price;
600 let y = profit - prev_profit;
601
602 let dy = -prev_profit / y;
603 breakevens.push(Breakeven {
604 price: prev_price + x * dy,
605 is_ascending: prev_profit.is_negative(),
606 });
607 }
608
609 prev_price = strike_price;
610 prev_profit = profit;
611 }
612
613 StrategyBreakevens { breakevens }
614}
615
616#[derive(Clone, Debug, Eq, PartialEq)]
617pub struct StrategyProfitBounds {
618 pub max_loss: Option<ProfitBound>,
619 pub max_profit: Option<ProfitBound>,
620}
621
622impl StrategyProfitBounds {
623 pub fn to_percentage_of_max_profit(&self, profit: Rational64) -> Option<f64> {
624 self.max_profit
625 .as_ref()
626 .and_then(|b| b.finite_value())
627 .map(|value| {
628 debug_assert!(value.is_positive());
629 (profit / value.abs()).to_f64().unwrap()
630 })
631 }
632}
633
634#[derive(Clone, Debug, Eq, PartialEq)]
635pub enum ProfitBound {
636 Infinite,
637 Finite {
638 value: Rational64,
639 price: Rational64,
640 },
641}
642
643impl ProfitBound {
644 pub fn finite_value(&self) -> Option<Rational64> {
645 match self {
646 ProfitBound::Finite { value, .. } => Some(*value),
647 _ => None,
648 }
649 }
650}
651
652pub fn calculate_profit_bounds_for_strategy(positions: &[Position]) -> StrategyProfitBounds {
653 if positions.is_empty() {
654 return StrategyProfitBounds {
655 max_loss: None,
656 max_profit: None,
657 };
658 }
659
660 let mut min_gradient = Rational64::zero();
661 let mut max_gradient = Rational64::zero();
662 for position in positions {
663 let gradient_delta = position.quantity() * position.equivalent_lot_size();
664 match (position.equivalent_option_type(), position.is_long()) {
665 (OptionType::Call, true) => {
666 max_gradient += gradient_delta;
667 }
668 (OptionType::Call, false) => {
669 max_gradient -= gradient_delta;
670 }
671 (OptionType::Put, true) => {
672 min_gradient += gradient_delta;
673 }
674 (OptionType::Put, false) => {
675 min_gradient -= gradient_delta;
676 }
677 }
678 }
679
680 let max_loss_at_strike = {
681 let mut max_loss = Rational64::from_integer(i64::MAX);
682 let mut max_loss_price = Rational64::zero();
683 for position in positions {
684 let price = position.equivalent_strike_price();
685 let profit_at_price = positions.iter().map(|o| o.profit_at_expiry(price)).sum();
686 if profit_at_price < max_loss {
687 max_loss = profit_at_price;
688 max_loss_price = price;
689 }
690 }
691 ProfitBound::Finite {
692 value: max_loss,
693 price: max_loss_price,
694 }
695 };
696
697 let max_profit_at_strike = {
698 let mut max_profit = Rational64::from_integer(i64::MIN);
699 let mut max_profit_price = Rational64::zero();
700 for position in positions {
701 let price = position.equivalent_strike_price();
702 let profit_at_price = positions.iter().map(|o| o.profit_at_expiry(price)).sum();
703 if profit_at_price > max_profit {
704 max_profit = profit_at_price;
705 max_profit_price = price;
706 }
707 }
708 ProfitBound::Finite {
709 value: max_profit,
710 price: max_profit_price,
711 }
712 };
713
714 let max_loss = if max_gradient.is_negative() {
715 ProfitBound::Infinite
716 } else if min_gradient.is_negative() {
717 let price = Rational64::zero();
718 let profit_at_zero = positions.iter().map(|o| o.profit_at_expiry(price)).sum();
719
720 if max_loss_at_strike.finite_value().unwrap() <= profit_at_zero {
722 max_loss_at_strike
723 } else {
724 ProfitBound::Finite {
725 value: profit_at_zero,
726 price,
727 }
728 }
729 } else {
730 max_loss_at_strike
731 };
732
733 let max_profit = if max_gradient.is_positive() {
734 ProfitBound::Infinite
735 } else if min_gradient.is_positive() {
736 let price = Rational64::zero();
737 let profit_at_zero = positions.iter().map(|o| o.profit_at_expiry(price)).sum();
738
739 if max_profit_at_strike.finite_value().unwrap() >= profit_at_zero {
741 max_profit_at_strike
742 } else {
743 ProfitBound::Finite {
744 value: profit_at_zero,
745 price,
746 }
747 }
748 } else {
749 max_profit_at_strike
750 };
751
752 StrategyProfitBounds {
753 max_loss: Some(max_loss).filter(|b| {
754 let finite = b.finite_value();
755 finite.is_none() || finite.filter(|v| v.is_negative()).is_some()
756 }),
757 max_profit: Some(max_profit).filter(|b| {
758 let finite = b.finite_value();
759 finite.is_none() || finite.filter(|v| v.is_positive()).is_some()
760 }),
761 }
762}
763
764pub trait ExpirationImpliedVolatilityProvider {
765 fn find_iv_for_expiration_date(&self, date: ExpirationDate) -> Option<f64>;
766}
767
768pub fn calculate_pop_for_breakevens(
769 breakevens: &StrategyBreakevens,
770 profit_bounds: &StrategyProfitBounds,
771 underlying_price: Rational64,
772 iv_provider: &impl ExpirationImpliedVolatilityProvider,
773 expiration_date: ExpirationDate,
774 now: Option<fn() -> DateTime<Utc>>,
775) -> Option<i32> {
776 if breakevens.min().is_none() && breakevens.max().is_none() {
777 if profit_bounds.max_loss.is_none() {
778 return Some(100);
779 } else {
780 debug_assert!(profit_bounds.max_profit.is_none());
781 return Some(0);
782 }
783 }
784
785 let mut pop = if breakevens.breakevens.first().unwrap().is_ascending {
786 0.0
787 } else {
788 1.0
789 };
790
791 for Breakeven {
792 price,
793 is_ascending,
794 } in &breakevens.breakevens
795 {
796 if *is_ascending {
797 pop += calculate_probability_of_expiring_gt_price(
798 *price,
799 underlying_price,
800 iv_provider,
801 expiration_date,
802 now,
803 )?;
804 } else {
805 pop -= calculate_probability_of_expiring_gt_price(
806 *price,
807 underlying_price,
808 iv_provider,
809 expiration_date,
810 now,
811 )?;
812 }
813 }
814
815 Some((pop * 100.0).round() as i32)
816}
817
818fn calculate_probability_of_expiring_gt_price(
819 price: Rational64,
820 underlying_price: Rational64,
821 iv_provider: &impl ExpirationImpliedVolatilityProvider,
822 expiration_date: ExpirationDate,
823 now: Option<fn() -> DateTime<Utc>>,
824) -> Option<f64> {
825 let stock_price = underlying_price.to_f64()?;
826 let expiration_implied_volatility = iv_provider.find_iv_for_expiration_date(expiration_date)?;
827
828 let num_minutes: u32 = expiration_date
829 .time_to_expiration(now)
830 .num_minutes()
831 .try_into()
832 .ok()?;
833 let year_minutes: u32 = Duration::days(365).num_minutes().try_into().ok()?;
834 let time = f64::from(num_minutes) / f64::from(year_minutes);
835 let vol = expiration_implied_volatility * time.sqrt();
836
837 let interest_rate = 0.05;
839 let d2 =
840 ((interest_rate - 0.5 * vol * vol) * time - (price.to_f64()? / stock_price).ln()) / vol;
841
842 use statrs::distribution::{Normal, Univariate};
843 let prob = Normal::new(0.0, 1.0).unwrap().cdf(d2);
844
845 Some(prob)
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 use chrono::{NaiveDate, TimeZone};
853 use num_traits::One;
854
855 #[test]
856 fn test_decimal_from_str() {
857 assert_eq!(
858 Decimal::from_str("0.3").unwrap(),
859 Decimal(Rational64::new(3, 10))
860 );
861 assert_eq!(
862 Decimal::from_str("-0.3").unwrap(),
863 Decimal(Rational64::new(-3, 10))
864 );
865 assert_eq!(
866 Decimal::from_str("9.12").unwrap(),
867 Decimal(Rational64::new(912, 100))
868 );
869 assert_eq!(
870 Decimal::from_str("-9.12").unwrap(),
871 Decimal(Rational64::new(-912, 100))
872 );
873 assert_eq!(
874 Decimal::from_str("23.012").unwrap(),
875 Decimal(Rational64::new(23012, 1000))
876 );
877 assert_eq!(
878 Decimal::from_str("1.0001").unwrap(),
879 Decimal(Rational64::new(10001, 10000))
880 );
881 assert_eq!(
882 Decimal::from_str("12,345.4321").unwrap(),
883 Decimal(Rational64::new(123454321, 10000))
884 );
885 assert_eq!(
886 Decimal::from_str("+2.1").unwrap(),
887 Decimal(Rational64::new(21, 10))
888 );
889 assert_eq!(
890 Decimal::from_str("2.").unwrap(),
891 Decimal(Rational64::new(2, 1))
892 );
893 assert_eq!(
894 Decimal::from_str("2").unwrap(),
895 Decimal(Rational64::new(2, 1))
896 );
897 }
898
899 #[test]
900 fn test_decimal_to_string() {
901 assert_eq!(
902 Decimal(Rational64::new(3, 10)).to_string(),
903 "0.3".to_string()
904 );
905 assert_eq!(
906 Decimal(Rational64::new(-3, 10)).to_string(),
907 "-0.3".to_string()
908 );
909 assert_eq!(
910 Decimal(Rational64::new(912, 100)).to_string(),
911 "9.12".to_string()
912 );
913 assert_eq!(
914 Decimal(Rational64::new(-912, 100)).to_string(),
915 "-9.12".to_string()
916 );
917 assert_eq!(
918 Decimal(Rational64::new(23012, 1000)).to_string(),
919 "23.012".to_string()
920 );
921 assert_eq!(
922 Decimal(Rational64::new(10001, 10000)).to_string(),
923 "1.0001".to_string()
924 );
925 assert_eq!(
926 Decimal(Rational64::new(123454321, 10000)).to_string(),
927 "12345.4321".to_string()
928 );
929 assert_eq!(
930 Decimal(Rational64::new(21, 10)).to_string(),
931 "2.1".to_string()
932 );
933 assert_eq!(Decimal(Rational64::new(2, 1)).to_string(), "2".to_string());
934 }
935
936 #[test]
937 fn test_decimal_to_str() {
938 assert_eq!(Decimal(Rational64::new(3, 10)).to_string(), "0.3",);
939 assert_eq!(Decimal(Rational64::new(-3, 10)).to_string(), "-0.3",);
940 assert_eq!(Decimal(Rational64::new(912, 100)).to_string(), "9.12",);
941 assert_eq!(Decimal(Rational64::new(-912, 100)).to_string(), "-9.12",);
942 assert_eq!(Decimal(Rational64::new(23012, 1000)).to_string(), "23.012",);
943 assert_eq!(Decimal(Rational64::new(10001, 10000)).to_string(), "1.0001",);
944 }
945
946 #[test]
947 fn test_short_call_profit_at_expiry() {
948 let option = OptionsPosition::mock(OptionType::Call, 100, 300, 1.into());
949 let underlying_price = Rational64::from_integer(101);
950 let profit = option.profit_at_expiry(underlying_price);
951
952 assert_eq!(profit, Rational64::from_integer(200));
953 }
954
955 #[test]
956 fn test_short_put_profit_at_expiry() {
957 let option = OptionsPosition::mock(OptionType::Put, 100, 300, 1.into());
958 let underlying_price = Rational64::from_integer(99);
959 let profit = option.profit_at_expiry(underlying_price);
960
961 assert_eq!(profit, Rational64::from_integer(200));
962 }
963
964 #[test]
965 fn test_long_put_profit_at_expiry() {
966 let option = OptionsPosition::mock(OptionType::Put, 100, -300, 1.into());
967 let underlying_price = Rational64::from_integer(99);
968 let profit = option.profit_at_expiry(underlying_price);
969
970 assert_eq!(profit, Rational64::from_integer(-200));
971 }
972
973 #[test]
974 fn test_calculate_breakevens_for_short_strangle() {
975 let options = [
976 OptionsPosition::mock(OptionType::Put, 20, 37, 1.into()),
977 OptionsPosition::mock(OptionType::Call, 28, 74, 1.into()),
978 ];
979
980 let breakevens = calculate_breakevens_for_strategy(&options);
981
982 assert_eq!(
983 breakevens,
984 StrategyBreakevens {
985 breakevens: vec![
986 Breakeven {
987 price: Rational64::new(1889, 100),
988 is_ascending: true
989 },
990 Breakeven {
991 price: Rational64::new(2911, 100),
992 is_ascending: false
993 }
994 ]
995 }
996 );
997 }
998
999 #[test]
1000 fn test_calculate_breakevens_for_short_call_ratio_spread() {
1001 let options = [
1002 OptionsPosition::mock(OptionType::Call, 15, -305, 1.into()),
1003 OptionsPosition::mock(OptionType::Call, 20, 217, 2.into()),
1004 ];
1005
1006 let breakevens = calculate_breakevens_for_strategy(&options);
1007
1008 assert_eq!(
1009 breakevens,
1010 StrategyBreakevens {
1011 breakevens: vec![Breakeven {
1012 price: Rational64::new(2629, 100),
1013 is_ascending: false
1014 }]
1015 }
1016 );
1017 }
1018
1019 #[test]
1020 fn test_calculate_profit_for_long_call() {
1021 let options = [OptionsPosition::mock(OptionType::Call, 20, -37, 1.into())];
1022
1023 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1024
1025 assert_eq!(
1026 profit_bounds,
1027 StrategyProfitBounds {
1028 max_loss: Some(ProfitBound::Finite {
1029 value: Rational64::from_integer(-37),
1030 price: Rational64::from_integer(20)
1031 }),
1032 max_profit: Some(ProfitBound::Infinite)
1033 }
1034 );
1035 }
1036
1037 #[test]
1038 fn test_calculate_profit_for_long_put() {
1039 let options = [OptionsPosition::mock(OptionType::Put, 20, -37, 1.into())];
1040
1041 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1042
1043 assert_eq!(
1044 profit_bounds,
1045 StrategyProfitBounds {
1046 max_loss: Some(ProfitBound::Finite {
1047 value: Rational64::from_integer(-37),
1048 price: Rational64::from_integer(20)
1049 }),
1050 max_profit: Some(ProfitBound::Finite {
1051 value: Rational64::from_integer(2000 - 37),
1052 price: Rational64::zero()
1053 })
1054 }
1055 );
1056 }
1057
1058 #[test]
1059 fn test_calculate_profit_for_short_strangle() {
1060 let options = [
1061 OptionsPosition::mock(OptionType::Put, 20, 37, 1.into()),
1062 OptionsPosition::mock(OptionType::Call, 28, 74, 1.into()),
1063 ];
1064
1065 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1066
1067 assert_eq!(
1068 profit_bounds,
1069 StrategyProfitBounds {
1070 max_loss: Some(ProfitBound::Infinite),
1071 max_profit: Some(ProfitBound::Finite {
1072 value: Rational64::from_integer(37 + 74),
1073 price: Rational64::from_integer(20)
1074 })
1075 }
1076 );
1077 }
1078
1079 #[test]
1080 fn test_calculate_profit_for_long_strangle() {
1081 let options = [
1082 OptionsPosition::mock(OptionType::Put, 20, -37, 1.into()),
1083 OptionsPosition::mock(OptionType::Call, 28, -74, 1.into()),
1084 ];
1085
1086 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1087
1088 assert_eq!(
1089 profit_bounds,
1090 StrategyProfitBounds {
1091 max_loss: Some(ProfitBound::Finite {
1092 value: Rational64::from_integer(-37 - 74),
1093 price: Rational64::from_integer(20)
1094 }),
1095 max_profit: Some(ProfitBound::Infinite),
1096 }
1097 );
1098 }
1099
1100 #[test]
1101 fn test_calculate_profit_for_short_call_ratio_spread() {
1102 let options = [
1103 OptionsPosition::mock(OptionType::Call, 15, -305, 1.into()),
1104 OptionsPosition::mock(OptionType::Call, 20, 217, 2.into()),
1105 ];
1106
1107 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1108
1109 assert_eq!(
1110 profit_bounds,
1111 StrategyProfitBounds {
1112 max_loss: Some(ProfitBound::Infinite),
1113 max_profit: Some(ProfitBound::Finite {
1114 value: Rational64::from_integer(-305 + 2 * 217 + 500),
1115 price: Rational64::from_integer(20)
1116 })
1117 }
1118 );
1119 }
1120
1121 #[test]
1122 fn test_calculate_profit_for_long_put_ratio_spread() {
1123 let options = [
1124 OptionsPosition::mock(OptionType::Put, 20, 305, 1.into()),
1125 OptionsPosition::mock(OptionType::Put, 15, -217, 2.into()),
1126 ];
1127
1128 let profit_bounds = calculate_profit_bounds_for_strategy(&options);
1129
1130 let max_loss = 305 - 2 * 217 - 500;
1131 let max_profit = max_loss + 1500;
1132 assert_eq!(
1133 profit_bounds,
1134 StrategyProfitBounds {
1135 max_loss: Some(ProfitBound::Finite {
1136 value: Rational64::from_integer(max_loss),
1137 price: Rational64::from_integer(15)
1138 }),
1139 max_profit: Some(ProfitBound::Finite {
1140 value: Rational64::from_integer(max_profit),
1141 price: Rational64::from_integer(0)
1142 }),
1143 }
1144 );
1145 }
1146
1147 #[test]
1148 fn test_calculate_profit_for_covered_call() {
1149 let positions = [
1150 SharesPosition::mock(-11, 200.into()),
1151 OptionsPosition::mock(OptionType::Call, 20, 30, 2.into()),
1152 ];
1153
1154 let profit_bounds = calculate_profit_bounds_for_strategy(&positions);
1155
1156 let max_loss = -11 * 200 + 30 * 2;
1157 let max_profit = (20 - 11) * 200 + 30 * 2;
1158 assert_eq!(
1159 profit_bounds,
1160 StrategyProfitBounds {
1161 max_loss: Some(ProfitBound::Finite {
1162 value: Rational64::from_integer(max_loss),
1163 price: Rational64::from_integer(0)
1164 }),
1165 max_profit: Some(ProfitBound::Finite {
1166 value: Rational64::from_integer(max_profit),
1167 price: Rational64::from_integer(20)
1168 }),
1169 }
1170 );
1171 }
1172
1173 #[test]
1174 fn test_calculate_breakevens_for_shares() {
1175 let positions = [
1176 SharesPosition::mock(-11, 150.into()), SharesPosition::mock(19, 50.into()), ];
1179 let breakevens = calculate_breakevens_for_strategy(&positions);
1180 assert_eq!(
1181 breakevens,
1182 StrategyBreakevens {
1183 breakevens: vec![Breakeven {
1184 price: Rational64::from_integer(7),
1185 is_ascending: true,
1186 }]
1187 }
1188 );
1189 }
1190
1191 #[test]
1192 fn test_calculate_breakevens_for_leap() {
1193 let positions = [OptionsPosition::mock(OptionType::Call, 1, -30, 1.into())];
1194 let breakevens = calculate_breakevens_for_strategy(&positions);
1195 assert_eq!(
1196 breakevens,
1197 StrategyBreakevens {
1198 breakevens: vec![Breakeven {
1199 price: Rational64::new(13, 10),
1200 is_ascending: true,
1201 }]
1202 }
1203 );
1204 }
1205
1206 #[test]
1207 fn test_calculate_pop_no_loss() {
1208 struct IVProvider;
1209 impl ExpirationImpliedVolatilityProvider for IVProvider {
1210 fn find_iv_for_expiration_date(&self, _: ExpirationDate) -> Option<f64> {
1211 None
1212 }
1213 }
1214
1215 let pop = calculate_pop_for_breakevens(
1216 &StrategyBreakevens { breakevens: vec![] },
1217 &StrategyProfitBounds {
1218 max_loss: None,
1219 max_profit: Some(ProfitBound::Infinite),
1220 },
1221 Rational64::one(),
1222 &IVProvider,
1223 ExpirationDate(NaiveDate::from_ymd(2020, 10, 16)),
1224 None,
1225 );
1226 assert_eq!(pop, Some(100));
1227 }
1228
1229 #[test]
1230 fn test_calculate_pop_multiple_breakevens() {
1231 let option_positions = [
1232 OptionsPosition::mock(OptionType::Put, 20, -55, 2.into()),
1233 OptionsPosition::mock(OptionType::Put, 30, 277, 1.into()),
1234 OptionsPosition::mock(OptionType::Call, 60, -823, 1.into()),
1235 OptionsPosition::mock(OptionType::Call, 85, 456, 2.into()),
1236 ];
1237
1238 let breakevens = calculate_breakevens_for_strategy(&option_positions);
1239
1240 assert_eq!(
1241 breakevens,
1242 StrategyBreakevens {
1243 breakevens: vec![
1244 Breakeven {
1245 price: Rational64::new(314, 25),
1246 is_ascending: false,
1247 },
1248 Breakeven {
1249 price: Rational64::new(686, 25),
1250 is_ascending: true,
1251 },
1252 Breakeven {
1253 price: Rational64::new(2814, 25),
1254 is_ascending: false,
1255 },
1256 ]
1257 }
1258 );
1259
1260 fn expiration_date() -> ExpirationDate {
1261 ExpirationDate(NaiveDate::from_ymd(2020, 10, 16))
1262 }
1263
1264 struct IVProvider;
1265 impl ExpirationImpliedVolatilityProvider for IVProvider {
1266 fn find_iv_for_expiration_date(&self, date: ExpirationDate) -> Option<f64> {
1267 if date == expiration_date() {
1268 Some(2.00)
1269 } else {
1270 None
1271 }
1272 }
1273 }
1274
1275 let profit_bounds = calculate_profit_bounds_for_strategy(&option_positions);
1276
1277 assert_eq!(
1278 profit_bounds,
1279 StrategyProfitBounds {
1280 max_loss: Some(ProfitBound::Infinite),
1281 max_profit: Some(ProfitBound::Finite {
1282 value: Rational64::from_integer(2756),
1283 price: Rational64::from_integer(85)
1284 },),
1285 }
1286 );
1287
1288 let pop = calculate_pop_for_breakevens(
1289 &breakevens,
1290 &profit_bounds,
1291 Rational64::new(475, 10),
1292 &IVProvider,
1293 expiration_date(),
1294 Some(|| Utc.ymd(2020, 9, 18).and_hms(1, 1, 1)),
1295 );
1296 assert_eq!(pop, Some(79));
1297 }
1298}