1use crate::error::position::PositionValidationErrorKind;
8use crate::error::{PositionError, TradeError};
9use crate::model::decimal::{d_add, d_mul, d_sub};
10use crate::model::expiration::resolve_expiration_date;
11use crate::model::trade::TradeStatusAble;
12use crate::model::types::{Action, OptionStyle, Side};
13use crate::model::{ExpirationDate, Options};
14use crate::model::{Trade, TradeAble, TradeStatus};
15use chrono::{DateTime, Utc};
16use positive::Positive;
17use rust_decimal::Decimal;
18use serde::{Deserialize, Serialize};
19use tracing::debug;
20
21#[derive(Clone, PartialEq, Serialize, Deserialize)]
72#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
73pub struct Position {
74 pub option: Options,
77
78 pub premium: Positive,
84
85 pub date: DateTime<Utc>,
88
89 pub open_fee: Positive,
93
94 pub close_fee: Positive,
98
99 pub epic: Option<String>,
101
102 pub extra_fields: Option<serde_json::Value>,
104}
105
106impl Position {
107 #[inline]
117 fn premium_per_contract(&self) -> Result<Positive, PositionError> {
118 Ok(self.premium.checked_mul(&self.option.contract_size)?)
119 }
120
121 #[must_use]
168 pub fn new(
169 option: Options,
170 premium: Positive,
171 date: DateTime<Utc>,
172 open_fee: Positive,
173 close_fee: Positive,
174 epic: Option<String>,
175 extra_fields: Option<serde_json::Value>,
176 ) -> Self {
177 Position {
178 option,
179 premium,
180 date,
181 open_fee,
182 close_fee,
183 epic,
184 extra_fields,
185 }
186 }
187
188 pub fn total_cost(&self) -> Result<Positive, PositionError> {
213 let total_cost = match self.option.side {
214 Side::Long => self
215 .premium_per_contract()?
216 .checked_add(&self.open_fee)?
217 .checked_add(&self.close_fee)?
218 .checked_mul(&self.option.quantity)?,
219 Side::Short => self.fees()?,
220 };
221
222 Ok(total_cost)
223 }
224
225 pub fn premium_received(&self) -> Result<Positive, PositionError> {
276 match self.option.side {
277 Side::Long => Ok(Positive::ZERO),
278 Side::Short => Ok(self
279 .premium_per_contract()?
280 .checked_mul(&self.option.quantity)?),
281 }
282 }
283
284 pub fn net_premium_received(&self) -> Result<Positive, PositionError> {
313 match self.option.side {
314 Side::Long => Ok(Positive::ZERO),
315 Side::Short => {
316 let premium = self
318 .premium_per_contract()?
319 .checked_mul(&self.option.quantity)?;
320 let total_cost = self.total_cost()?;
321 if premium >= total_cost {
322 Ok(premium.checked_sub(&total_cost)?)
323 } else {
324 Ok(Positive::ZERO)
325 }
326 }
327 }
328 }
329
330 pub fn pnl_at_expiration(&self, price: &Option<&Positive>) -> Result<Decimal, PositionError> {
385 let intrinsic = match price {
391 None => self.option.intrinsic_value(self.option.underlying_price)?,
392 Some(price) => self.option.intrinsic_value(**price)?,
393 };
394 let cost = self.total_cost()?.to_dec();
395 let premium_recv = self.premium_received()?.to_dec();
396 let net_after_cost = d_sub(intrinsic, cost, "position::pnl_at_expiration::net")?;
397 d_add(
398 net_after_cost,
399 premium_recv,
400 "position::pnl_at_expiration::total",
401 )
402 .map_err(PositionError::from)
403 }
404
405 pub fn unrealized_pnl(&self, price: Positive) -> Result<Decimal, PositionError> {
462 let per_contract = match self.option.side {
469 Side::Long => {
470 let after_premium = d_mul(
471 d_sub(
472 price.to_dec(),
473 self.premium.to_dec(),
474 "position::unrealized_pnl::long::after_premium",
475 )?,
476 self.option.contract_size.to_dec(),
477 "position::unrealized_pnl::long::contract_size",
478 )?;
479 let after_open = d_sub(
480 after_premium,
481 self.open_fee.to_dec(),
482 "position::unrealized_pnl::long::after_open_fee",
483 )?;
484 d_sub(
485 after_open,
486 self.close_fee.to_dec(),
487 "position::unrealized_pnl::long::net",
488 )?
489 }
490 Side::Short => {
491 let after_price = d_mul(
492 d_sub(
493 self.premium.to_dec(),
494 price.to_dec(),
495 "position::unrealized_pnl::short::after_price",
496 )?,
497 self.option.contract_size.to_dec(),
498 "position::unrealized_pnl::short::contract_size",
499 )?;
500 let after_open = d_sub(
501 after_price,
502 self.open_fee.to_dec(),
503 "position::unrealized_pnl::short::after_open_fee",
504 )?;
505 d_sub(
506 after_open,
507 self.close_fee.to_dec(),
508 "position::unrealized_pnl::short::net",
509 )?
510 }
511 };
512 Ok(d_mul(
513 per_contract,
514 self.option.quantity.to_dec(),
515 "position::unrealized_pnl::scaled",
516 )?)
517 }
518
519 pub fn days_held(&self) -> Result<Positive, PositionError> {
539 let days = Utc::now().signed_duration_since(self.date).num_days() as f64;
542 Positive::new(days).map_err(|e| {
543 PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
544 reason: format!("failed to calculate days held: {}", e),
545 })
546 })
547 }
548
549 pub fn days_to_expiration(&self) -> Result<Positive, PositionError> {
570 match self.option.expiration_date {
571 ExpirationDate::Days(days) => Ok(days),
572 ExpirationDate::DateTime(datetime) => {
573 let days = datetime.signed_duration_since(Utc::now()).num_days() as f64;
574 Positive::new(days.max(0.0)).map_err(|e| {
575 PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
576 reason: format!("failed to calculate days to expiration: {}", e),
577 })
578 })
579 }
580 }
581 }
582
583 #[must_use]
594 pub fn is_long(&self) -> bool {
595 match self.option.side {
596 Side::Long => true,
597 Side::Short => false,
598 }
599 }
600
601 #[must_use]
612 pub fn is_short(&self) -> bool {
613 match self.option.side {
614 Side::Long => false,
615 Side::Short => true,
616 }
617 }
618
619 pub fn net_cost(&self) -> Result<Decimal, PositionError> {
640 match self.option.side {
641 Side::Long => Ok(self.total_cost()?.to_dec()),
642 Side::Short => {
643 let fees = self.fees()?.to_dec();
644 let premium = self.premium_received()?.to_dec();
645 Ok(d_sub(fees, premium, "Position::net_cost/short")?)
646 }
647 }
648 }
649
650 #[must_use]
679 pub fn break_even(&self) -> Option<Positive> {
680 if self.option.quantity == Positive::ZERO {
681 return None;
682 }
683 let position_total_cost = self.total_cost().ok()?;
684 let position_size = self.option.position_size().ok()?;
688 let total_cost_per_unit = position_total_cost.checked_div(&position_size).ok()?;
689 match (&self.option.side, &self.option.option_style) {
690 (Side::Long, OptionStyle::Call) => self
691 .option
692 .strike_price
693 .checked_add(&total_cost_per_unit)
694 .ok(),
695 (Side::Short, OptionStyle::Call) => self
696 .option
697 .strike_price
698 .checked_add(&self.premium)
699 .and_then(|total| total.checked_sub(&total_cost_per_unit))
700 .ok(),
701 (Side::Long, OptionStyle::Put) => self
702 .option
703 .strike_price
704 .checked_sub(&total_cost_per_unit)
705 .ok(),
706 (Side::Short, OptionStyle::Put) => self
707 .option
708 .strike_price
709 .checked_sub(&self.premium)
710 .and_then(|net| net.checked_add(&total_cost_per_unit))
711 .ok(),
712 }
713 }
714
715 #[allow(dead_code)]
728 pub(crate) fn max_profit(&self) -> Result<Positive, PositionError> {
729 match self.option.side {
730 Side::Long => Ok(Positive::MAX),
731 Side::Short => self.net_premium_received(),
732 }
733 }
734
735 #[allow(dead_code)]
748 pub(crate) fn max_loss(&self) -> Result<Positive, PositionError> {
749 match self.option.side {
750 Side::Long => self.total_cost(),
751 Side::Short => Ok(Positive::MAX),
752 }
753 }
754
755 pub fn fees(&self) -> Result<Positive, PositionError> {
773 Ok(self
774 .open_fee
775 .checked_add(&self.close_fee)?
776 .checked_mul(&self.option.quantity)?)
777 }
778
779 pub fn validate(&self) -> bool {
816 if self.option.side == Side::Short && self.premium == Positive::ZERO {
817 debug!("Premium must be greater than zero for short positions.");
818 return false;
819 }
820 if !self.option.validate() {
821 debug!("Option is not valid.");
822 return false;
823 }
824 true
825 }
826}
827
828impl Default for Position {
829 fn default() -> Self {
830 Position {
831 option: Options::default(),
832 premium: Positive::ZERO,
833 date: Utc::now(),
834 open_fee: Positive::ZERO,
835 close_fee: Positive::ZERO,
836 epic: None,
837 extra_fields: None,
838 }
839 }
840}
841
842impl TradeAble for Position {
843 fn trade(&self) -> Result<Trade, TradeError> {
844 let fee = self
845 .open_fee
846 .checked_add(&self.close_fee)
847 .map_err(|error| {
848 TradeError::invalid_trade(&format!(
849 "open and close fees overflow the Positive range: {error}"
850 ))
851 })?;
852 self.premium_per_contract().map_err(|error| {
857 TradeError::invalid_trade(&format!(
858 "premium times contract size overflows the Positive range: {error}"
859 ))
860 })?;
861 if let (Ok(expiry), Some(timestamp)) = (
862 resolve_expiration_date(&self.option.expiration_date),
863 Utc::now().timestamp_nanos_opt(),
864 ) {
865 Ok(Trade {
866 id: uuid::Uuid::new_v4(),
867 action: Action::Buy,
868 side: self.option.side,
869 option_style: self.option.option_style,
870 fee,
871 symbol: None,
872 strike: self.option.strike_price,
873 expiry,
874 timestamp,
875 quantity: self.option.quantity,
876 premium: self.premium,
877 contract_size: self.option.contract_size,
878 underlying_price: self.option.underlying_price,
879 notes: None,
880 status: TradeStatus::Other("Not yet initialized".to_string()),
881 })
882 } else {
883 Err(TradeError::invalid_trade(
884 "Could not create trade from position",
885 ))
886 }
887 }
888
889 fn trade_ref(&self) -> Result<&Trade, TradeError> {
890 Err(TradeError::invalid_trade(
891 "trade_ref() is not implemented for Position",
892 ))
893 }
894
895 fn trade_mut(&mut self) -> Result<&mut Trade, TradeError> {
896 Err(TradeError::invalid_trade(
897 "trade_mut() is not implemented for Position",
898 ))
899 }
900}
901
902impl TradeStatusAble for Position {
903 fn open(&self) -> Result<Trade, TradeError> {
904 let mut trade = self.trade()?;
905 trade.status = TradeStatus::Open;
906 Ok(trade)
907 }
908
909 fn close(&self) -> Result<Trade, TradeError> {
910 let mut trade = self.trade()?;
911 if trade.premium.to_dec() <= rust_decimal_macros::dec!(0.01) {
917 trade.premium = Positive::ZERO;
918 }
919 trade.status = TradeStatus::Closed;
920 trade.action = Action::Sell;
921 Ok(trade)
922 }
923
924 fn expired(&self) -> Result<Trade, TradeError> {
925 let mut trade = self.trade()?;
926 trade.status = TradeStatus::Expired;
927 trade.action = Action::Sell;
928 Ok(trade)
929 }
930
931 fn exercised(&self) -> Result<Trade, TradeError> {
932 let mut trade = self.trade()?;
933 trade.status = TradeStatus::Exercised;
934 trade.action = Action::Sell;
935 Ok(trade)
936 }
937
938 fn assigned(&self) -> Result<Trade, TradeError> {
939 let mut trade = self.trade()?;
940 trade.status = TradeStatus::Assigned;
941 trade.action = Action::Other;
942 Ok(trade)
943 }
944
945 fn status_other(&self) -> Result<Trade, TradeError> {
946 let mut trade = self.trade()?;
947 trade.status = TradeStatus::Other("Not yet initialized".to_string());
948 trade.action = Action::Other;
949 Ok(trade)
950 }
951}
952
953#[cfg(test)]
954mod tests_position {
955 use super::*;
956
957 use crate::constants::ZERO;
958 use crate::model::types::{OptionStyle, OptionType, Side};
959
960 use chrono::Duration;
961 use num_traits::ToPrimitive;
962 use positive::pos_or_panic;
963 use rust_decimal_macros::dec;
964
965 fn setup_option(
966 side: Side,
967 option_style: OptionStyle,
968 strike_price: Positive,
969 underlying_price: Positive,
970 quantity: Positive,
971 expiration_days: Positive,
972 ) -> Options {
973 Options {
974 option_type: OptionType::European,
975 side,
976 underlying_symbol: "".to_string(),
977 strike_price,
978 expiration_date: ExpirationDate::Days(expiration_days),
979 implied_volatility: pos_or_panic!(0.2),
980 quantity,
981 contract_size: Positive::ONE,
982 underlying_price,
983 risk_free_rate: dec!(0.01),
984 option_style,
985 dividend_yield: Positive::ZERO,
986 exotic_params: None,
987 }
988 }
989
990 #[test]
991 fn test_position_total_cost() {
992 let option = setup_option(
993 Side::Long,
994 OptionStyle::Call,
995 Positive::HUNDRED,
996 pos_or_panic!(105.0),
997 Positive::ONE,
998 pos_or_panic!(30.0),
999 );
1000 let position = Position::new(
1001 option,
1002 pos_or_panic!(5.0),
1003 Utc::now(),
1004 Positive::ONE,
1005 Positive::ONE,
1006 None,
1007 None,
1008 );
1009 assert_eq!(
1010 position.total_cost().unwrap(),
1011 7.0,
1012 "Total cost calculation is incorrect."
1013 );
1014 }
1015
1016 #[test]
1017 fn test_position_total_cost_size() {
1018 let option = setup_option(
1019 Side::Long,
1020 OptionStyle::Call,
1021 Positive::HUNDRED,
1022 pos_or_panic!(105.0),
1023 pos_or_panic!(10.0),
1024 pos_or_panic!(30.0),
1025 );
1026 let position = Position::new(
1027 option,
1028 pos_or_panic!(5.0),
1029 Utc::now(),
1030 Positive::ONE,
1031 Positive::ONE,
1032 None,
1033 None,
1034 );
1035 assert_eq!(
1036 position.total_cost().unwrap(),
1037 70.0,
1038 "Total cost calculation is incorrect."
1039 );
1040 }
1041
1042 #[test]
1043 fn test_position_total_cost_short() {
1044 let option = setup_option(
1045 Side::Short,
1046 OptionStyle::Call,
1047 Positive::HUNDRED,
1048 pos_or_panic!(105.0),
1049 Positive::ONE,
1050 pos_or_panic!(30.0),
1051 );
1052 let position = Position::new(
1053 option,
1054 pos_or_panic!(5.0),
1055 Utc::now(),
1056 Positive::ONE,
1057 Positive::ONE,
1058 None,
1059 None,
1060 );
1061 assert_eq!(
1062 position.total_cost().unwrap(),
1063 2.0,
1064 "Total cost calculation is incorrect."
1065 );
1066 }
1067
1068 #[test]
1069 fn test_position_total_cost_short_size() {
1070 let option = setup_option(
1071 Side::Short,
1072 OptionStyle::Call,
1073 Positive::HUNDRED,
1074 pos_or_panic!(105.0),
1075 pos_or_panic!(10.0),
1076 pos_or_panic!(30.0),
1077 );
1078 let position = Position::new(
1079 option,
1080 pos_or_panic!(5.0),
1081 Utc::now(),
1082 Positive::ONE,
1083 Positive::ONE,
1084 None,
1085 None,
1086 );
1087 assert_eq!(
1088 position.total_cost().unwrap(),
1089 20.0,
1090 "Total cost calculation is incorrect."
1091 );
1092 }
1093
1094 #[test]
1095 fn test_position_check_negative_premium() {
1096 let option = setup_option(
1097 Side::Long,
1098 OptionStyle::Call,
1099 Positive::HUNDRED,
1100 pos_or_panic!(110.0),
1101 Positive::ONE,
1102 Positive::ZERO,
1103 );
1104 let position = Position::new(
1105 option,
1106 pos_or_panic!(5.0),
1107 Utc::now(),
1108 Positive::ONE,
1109 Positive::ONE,
1110 None,
1111 None,
1112 );
1113 assert_eq!(
1114 position.pnl_at_expiration(&None).unwrap(),
1115 dec!(3.0),
1116 "PNL at expiration for long call ITM is incorrect."
1117 );
1118 }
1119
1120 #[test]
1121 fn test_position_pnl_at_expiration_long_call_itm() {
1122 let option = setup_option(
1123 Side::Long,
1124 OptionStyle::Call,
1125 Positive::HUNDRED,
1126 pos_or_panic!(110.0),
1127 Positive::ONE,
1128 Positive::ZERO,
1129 );
1130 let position = Position::new(
1131 option,
1132 pos_or_panic!(5.0),
1133 Utc::now(),
1134 Positive::ONE,
1135 Positive::ONE,
1136 None,
1137 None,
1138 );
1139 assert_eq!(
1140 position.pnl_at_expiration(&None).unwrap(),
1141 dec!(3.0),
1142 "PNL at expiration for long call ITM is incorrect."
1143 );
1144 }
1145
1146 #[test]
1147 fn test_position_pnl_at_expiration_long_call_itm_quantity() {
1148 let option = setup_option(
1149 Side::Long,
1150 OptionStyle::Call,
1151 Positive::HUNDRED,
1152 pos_or_panic!(110.0),
1153 pos_or_panic!(10.0),
1154 Positive::ZERO,
1155 );
1156 let position = Position::new(
1157 option,
1158 pos_or_panic!(5.0),
1159 Utc::now(),
1160 Positive::ONE,
1161 Positive::ONE,
1162 None,
1163 None,
1164 );
1165 assert_eq!(
1166 position.pnl_at_expiration(&None).unwrap(),
1167 dec!(30.0),
1168 "PNL at expiration for long call ITM is incorrect."
1169 );
1170 }
1171
1172 #[test]
1173 fn test_position_pnl_at_expiration_short_call_itm() {
1174 let option = setup_option(
1175 Side::Short,
1176 OptionStyle::Call,
1177 Positive::HUNDRED,
1178 pos_or_panic!(110.0),
1179 Positive::ONE,
1180 Positive::ZERO,
1181 );
1182 let position = Position::new(
1183 option,
1184 pos_or_panic!(5.0),
1185 Utc::now(),
1186 Positive::ONE,
1187 Positive::ONE,
1188 None,
1189 None,
1190 );
1191 assert_eq!(
1192 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1193 -7.0,
1194 "PNL at expiration for short call ITM is incorrect."
1195 );
1196 }
1197
1198 #[test]
1199 fn test_position_pnl_at_expiration_short_call_itm_quantity() {
1200 let option = setup_option(
1201 Side::Short,
1202 OptionStyle::Call,
1203 Positive::HUNDRED,
1204 pos_or_panic!(110.0),
1205 pos_or_panic!(10.0),
1206 Positive::ZERO,
1207 );
1208 let position = Position::new(
1209 option,
1210 pos_or_panic!(5.0),
1211 Utc::now(),
1212 Positive::ONE,
1213 Positive::ONE,
1214 None,
1215 None,
1216 );
1217 assert_eq!(
1218 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1219 -70.0,
1220 "PNL at expiration for short call ITM is incorrect."
1221 );
1222 }
1223
1224 #[test]
1225 fn test_position_pnl_at_expiration_long_put_itm() {
1226 let option = setup_option(
1227 Side::Long,
1228 OptionStyle::Put,
1229 Positive::HUNDRED,
1230 pos_or_panic!(90.0),
1231 Positive::ONE,
1232 Positive::ZERO,
1233 );
1234 let position = Position::new(
1235 option,
1236 pos_or_panic!(5.0),
1237 Utc::now(),
1238 Positive::ONE,
1239 Positive::ONE,
1240 None,
1241 None,
1242 );
1243 assert_eq!(
1244 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1245 3.0,
1246 "PNL at expiration for long put ITM is incorrect."
1247 );
1248 }
1249
1250 #[test]
1251 fn test_position_pnl_at_expiration_long_put_itm_quantity() {
1252 let option = setup_option(
1253 Side::Long,
1254 OptionStyle::Put,
1255 Positive::HUNDRED,
1256 pos_or_panic!(90.0),
1257 pos_or_panic!(10.0),
1258 Positive::ZERO,
1259 );
1260 let position = Position::new(
1261 option,
1262 pos_or_panic!(5.0),
1263 Utc::now(),
1264 Positive::ONE,
1265 Positive::ONE,
1266 None,
1267 None,
1268 );
1269 assert_eq!(
1270 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1271 30.0,
1272 "PNL at expiration for long put ITM is incorrect."
1273 );
1274 }
1275
1276 #[test]
1277 fn test_position_pnl_at_expiration_short_put_itm() {
1278 let option = setup_option(
1279 Side::Short,
1280 OptionStyle::Put,
1281 Positive::HUNDRED,
1282 pos_or_panic!(90.0),
1283 Positive::ONE,
1284 Positive::ZERO,
1285 );
1286 let position = Position::new(
1287 option,
1288 pos_or_panic!(5.0),
1289 Utc::now(),
1290 Positive::ONE,
1291 Positive::ONE,
1292 None,
1293 None,
1294 );
1295 assert_eq!(
1296 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1297 -7.0,
1298 "PNL at expiration for short put ITM is incorrect."
1299 );
1300 }
1301
1302 #[test]
1303 fn test_position_pnl_at_expiration_short_put_itm_quantity() {
1304 let option = setup_option(
1305 Side::Short,
1306 OptionStyle::Put,
1307 Positive::HUNDRED,
1308 pos_or_panic!(90.0),
1309 pos_or_panic!(10.0),
1310 Positive::ZERO,
1311 );
1312 let position = Position::new(
1313 option,
1314 pos_or_panic!(5.0),
1315 Utc::now(),
1316 Positive::ONE,
1317 Positive::ONE,
1318 None,
1319 None,
1320 );
1321 assert_eq!(
1322 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1323 -70.0,
1324 "PNL at expiration for short put ITM is incorrect."
1325 );
1326 }
1327
1328 #[test]
1329 fn test_position_pnl_at_expiration_short_put_itm_winning() {
1330 let option = setup_option(
1331 Side::Short,
1332 OptionStyle::Put,
1333 Positive::HUNDRED,
1334 pos_or_panic!(110.0),
1335 Positive::ONE,
1336 Positive::ZERO,
1337 );
1338 let position = Position::new(
1339 option,
1340 pos_or_panic!(5.0),
1341 Utc::now(),
1342 Positive::ONE,
1343 Positive::ONE,
1344 None,
1345 None,
1346 );
1347 assert_eq!(
1348 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1349 3.0,
1350 "PNL at expiration for short put ITM is incorrect."
1351 );
1352 }
1353
1354 #[test]
1355 fn test_position_pnl_at_expiration_short_put_itm_quantity_winning() {
1356 let option = setup_option(
1357 Side::Short,
1358 OptionStyle::Put,
1359 Positive::HUNDRED,
1360 pos_or_panic!(110.0),
1361 pos_or_panic!(10.0),
1362 Positive::ZERO,
1363 );
1364 let position = Position::new(
1365 option,
1366 pos_or_panic!(5.0),
1367 Utc::now(),
1368 Positive::ONE,
1369 Positive::ONE,
1370 None,
1371 None,
1372 );
1373 assert_eq!(
1374 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1375 30.0,
1376 "PNL at expiration for short put ITM is incorrect."
1377 );
1378 }
1379
1380 #[test]
1381 fn test_unrealized_pnl_long_call() {
1382 let option = setup_option(
1383 Side::Long,
1384 OptionStyle::Call,
1385 Positive::HUNDRED,
1386 pos_or_panic!(105.0),
1387 Positive::ONE,
1388 pos_or_panic!(30.0),
1389 );
1390 let position = Position::new(
1391 option,
1392 pos_or_panic!(5.0),
1393 Utc::now(),
1394 Positive::ONE,
1395 Positive::ONE,
1396 None,
1397 None,
1398 );
1399 assert_eq!(
1400 position
1401 .pnl_at_expiration(&Some(&pos_or_panic!(107.0)))
1402 .unwrap(),
1403 Positive::ZERO,
1404 "Unrealized PNL for long call is incorrect."
1405 );
1406 }
1407
1408 #[test]
1409 fn test_unrealized_pnl_long_call_quantity() {
1410 let option = setup_option(
1411 Side::Long,
1412 OptionStyle::Call,
1413 Positive::HUNDRED,
1414 pos_or_panic!(105.0),
1415 pos_or_panic!(10.0),
1416 pos_or_panic!(30.0),
1417 );
1418 let position = Position::new(
1419 option,
1420 pos_or_panic!(5.0),
1421 Utc::now(),
1422 Positive::ONE,
1423 Positive::ONE,
1424 None,
1425 None,
1426 );
1427 assert_eq!(
1428 position
1429 .pnl_at_expiration(&Some(&pos_or_panic!(107.0)))
1430 .unwrap()
1431 .to_f64()
1432 .unwrap(),
1433 ZERO,
1434 "Unrealized PNL for long call is incorrect."
1435 );
1436 }
1437
1438 #[test]
1439 fn test_unrealized_pnl_short_call() {
1440 let option = setup_option(
1441 Side::Short,
1442 OptionStyle::Call,
1443 Positive::HUNDRED,
1444 pos_or_panic!(105.0),
1445 pos_or_panic!(10.0),
1446 pos_or_panic!(30.0),
1447 );
1448 let position = Position::new(
1449 option,
1450 pos_or_panic!(5.0),
1451 Utc::now(),
1452 Positive::ONE,
1453 Positive::ONE,
1454 None,
1455 None,
1456 );
1457 assert_eq!(
1458 position
1459 .unrealized_pnl(pos_or_panic!(3.0))
1460 .unwrap()
1461 .to_f64()
1462 .unwrap(),
1463 ZERO,
1464 "Unrealized PNL for short call is incorrect."
1465 );
1466 }
1467
1468 #[test]
1469 fn test_unrealized_pnl_short_call_bis() {
1470 let option = setup_option(
1471 Side::Short,
1472 OptionStyle::Call,
1473 Positive::HUNDRED,
1474 pos_or_panic!(105.0),
1475 Positive::ONE,
1476 pos_or_panic!(30.0),
1477 );
1478 let position = Position::new(
1479 option,
1480 pos_or_panic!(5.0),
1481 Utc::now(),
1482 Positive::ONE,
1483 Positive::ONE,
1484 None,
1485 None,
1486 );
1487 assert_eq!(
1488 position
1489 .unrealized_pnl(pos_or_panic!(10.0))
1490 .unwrap()
1491 .to_f64()
1492 .unwrap(),
1493 -7.0,
1494 "Unrealized PNL for short call is incorrect."
1495 );
1496 }
1497
1498 #[test]
1499 fn test_days_held() {
1500 let option = setup_option(
1501 Side::Long,
1502 OptionStyle::Call,
1503 Positive::HUNDRED,
1504 pos_or_panic!(105.0),
1505 pos_or_panic!(10.0),
1506 pos_or_panic!(30.0),
1507 );
1508 let date = Utc::now() - Duration::days(10);
1509 let position = Position::new(
1510 option,
1511 pos_or_panic!(5.0),
1512 date,
1513 Positive::ONE,
1514 Positive::ONE,
1515 None,
1516 None,
1517 );
1518 assert_eq!(
1519 position.days_held().unwrap().to_f64(),
1520 10.0,
1521 "Days held calculation is incorrect."
1522 );
1523 }
1524
1525 #[test]
1526 fn test_days_to_expiration() {
1527 let option = setup_option(
1528 Side::Long,
1529 OptionStyle::Call,
1530 Positive::HUNDRED,
1531 pos_or_panic!(105.0),
1532 pos_or_panic!(10.0),
1533 pos_or_panic!(30.0),
1534 );
1535 let position = Position::new(
1536 option,
1537 pos_or_panic!(5.0),
1538 Utc::now(),
1539 Positive::ONE,
1540 Positive::ONE,
1541 None,
1542 None,
1543 );
1544 assert_eq!(
1545 position.days_to_expiration().unwrap().to_f64(),
1546 30.0,
1547 "Days to expiration calculation is incorrect."
1548 );
1549 }
1550
1551 #[test]
1552 fn test_is_long_position() {
1553 let option = setup_option(
1554 Side::Long,
1555 OptionStyle::Call,
1556 Positive::HUNDRED,
1557 pos_or_panic!(105.0),
1558 pos_or_panic!(10.0),
1559 pos_or_panic!(30.0),
1560 );
1561 let position = Position::new(
1562 option,
1563 pos_or_panic!(5.0),
1564 Utc::now(),
1565 Positive::ONE,
1566 Positive::ONE,
1567 None,
1568 None,
1569 );
1570 assert!(
1571 position.is_long(),
1572 "is_long should return true for long positions."
1573 );
1574 assert!(
1575 !position.is_short(),
1576 "is_short should return false for long positions."
1577 );
1578 }
1579
1580 #[test]
1581 fn test_is_short_position() {
1582 let option = setup_option(
1583 Side::Short,
1584 OptionStyle::Call,
1585 Positive::HUNDRED,
1586 pos_or_panic!(105.0),
1587 pos_or_panic!(10.0),
1588 pos_or_panic!(30.0),
1589 );
1590 let position = Position::new(
1591 option,
1592 pos_or_panic!(5.0),
1593 Utc::now(),
1594 Positive::ONE,
1595 Positive::ONE,
1596 None,
1597 None,
1598 );
1599 assert!(
1600 position.is_short(),
1601 "is_short should return true for short positions."
1602 );
1603 assert!(
1604 !position.is_long(),
1605 "is_long should return false for short positions."
1606 );
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests_valid_position {
1612 use super::*;
1613
1614 use crate::model::utils::create_sample_position;
1615 use positive::pos_or_panic;
1616
1617 #[test]
1618 fn test_valid_position() {
1619 let position = create_sample_position(
1620 OptionStyle::Call,
1621 Side::Short,
1622 pos_or_panic!(90.0),
1623 Positive::ONE,
1624 pos_or_panic!(95.0),
1625 pos_or_panic!(0.2),
1626 );
1627 assert!(position.validate());
1628 }
1629
1630 #[test]
1631 fn test_zero_premium() {
1632 let mut position = create_sample_position(
1633 OptionStyle::Call,
1634 Side::Short,
1635 pos_or_panic!(90.0),
1636 Positive::ONE,
1637 pos_or_panic!(95.0),
1638 pos_or_panic!(0.2),
1639 );
1640 position.premium = Positive::ZERO;
1641 assert!(!position.validate());
1642 }
1643
1644 #[test]
1645 fn test_invalid_option() {
1646 let mut position = create_sample_position(
1647 OptionStyle::Call,
1648 Side::Short,
1649 pos_or_panic!(90.0),
1650 Positive::ONE,
1651 pos_or_panic!(95.0),
1652 pos_or_panic!(0.2),
1653 );
1654 position.option.strike_price = Positive::ZERO; assert!(!position.validate());
1656 }
1657
1658 #[test]
1660 fn test_position_validate_negative_risk_free_rate_is_valid() {
1661 let mut position = create_sample_position(
1662 OptionStyle::Call,
1663 Side::Short,
1664 pos_or_panic!(90.0),
1665 Positive::ONE,
1666 pos_or_panic!(95.0),
1667 pos_or_panic!(0.2),
1668 );
1669 position.option.risk_free_rate = rust_decimal_macros::dec!(-0.01);
1670 assert!(position.validate());
1671 }
1672
1673 #[test]
1674 fn test_zero_fees() {
1675 let mut position = create_sample_position(
1676 OptionStyle::Call,
1677 Side::Short,
1678 pos_or_panic!(90.0),
1679 Positive::ONE,
1680 pos_or_panic!(95.0),
1681 pos_or_panic!(0.2),
1682 );
1683 position.open_fee = Positive::ZERO;
1684 position.close_fee = Positive::ZERO;
1685 assert!(position.validate());
1686 }
1687}
1688
1689#[cfg(test)]
1690mod tests_position_break_even {
1691 use super::*;
1692
1693 use crate::model::types::{OptionStyle, OptionType, Side};
1694
1695 use positive::pos_or_panic;
1696 use rust_decimal_macros::dec;
1697
1698 fn setup_option(
1699 side: Side,
1700 option_style: OptionStyle,
1701 strike_price: Positive,
1702 underlying_price: Positive,
1703 quantity: Positive,
1704 expiration_days: Positive,
1705 ) -> Options {
1706 Options {
1707 option_type: OptionType::European,
1708 side,
1709 underlying_symbol: "".to_string(),
1710 strike_price,
1711 expiration_date: ExpirationDate::Days(expiration_days),
1712 implied_volatility: pos_or_panic!(0.2),
1713 quantity,
1714 contract_size: Positive::ONE,
1715 underlying_price,
1716 risk_free_rate: dec!(0.01),
1717 option_style,
1718 dividend_yield: Positive::ZERO,
1719 exotic_params: None,
1720 }
1721 }
1722
1723 #[test]
1724 fn test_unrealized_pnl_long_call() {
1725 let option = setup_option(
1726 Side::Long,
1727 OptionStyle::Call,
1728 Positive::HUNDRED,
1729 pos_or_panic!(105.0),
1730 Positive::ONE,
1731 pos_or_panic!(30.0),
1732 );
1733 let position = Position::new(
1734 option,
1735 pos_or_panic!(5.0),
1736 Utc::now(),
1737 Positive::ONE,
1738 Positive::ONE,
1739 None,
1740 None,
1741 );
1742 assert_eq!(position.break_even().unwrap(), 107.0);
1743 }
1744
1745 #[test]
1746 fn test_unrealized_pnl_long_call_size() {
1747 let option = setup_option(
1748 Side::Long,
1749 OptionStyle::Call,
1750 Positive::HUNDRED,
1751 pos_or_panic!(105.0),
1752 pos_or_panic!(10.0),
1753 pos_or_panic!(30.0),
1754 );
1755 let position = Position::new(
1756 option,
1757 pos_or_panic!(5.0),
1758 Utc::now(),
1759 Positive::ONE,
1760 Positive::ONE,
1761 None,
1762 None,
1763 );
1764 assert_eq!(position.break_even().unwrap(), 107.0);
1765 }
1766
1767 #[test]
1768 fn test_unrealized_pnl_short_call() {
1769 let option = setup_option(
1770 Side::Short,
1771 OptionStyle::Call,
1772 Positive::HUNDRED,
1773 pos_or_panic!(105.0),
1774 Positive::ONE,
1775 pos_or_panic!(30.0),
1776 );
1777 let position = Position::new(
1778 option,
1779 pos_or_panic!(5.0),
1780 Utc::now(),
1781 Positive::ONE,
1782 Positive::ONE,
1783 None,
1784 None,
1785 );
1786 assert_eq!(position.break_even().unwrap(), 103.0);
1787 }
1788
1789 #[test]
1790 fn test_unrealized_pnl_short_call_size() {
1791 let option = setup_option(
1792 Side::Short,
1793 OptionStyle::Call,
1794 Positive::HUNDRED,
1795 pos_or_panic!(105.0),
1796 pos_or_panic!(10.0),
1797 pos_or_panic!(30.0),
1798 );
1799 let position = Position::new(
1800 option,
1801 pos_or_panic!(5.0),
1802 Utc::now(),
1803 Positive::ONE,
1804 Positive::ONE,
1805 None,
1806 None,
1807 );
1808 assert_eq!(position.break_even().unwrap(), 103.0);
1809 }
1810
1811 #[test]
1812 fn test_unrealized_pnl_long_put() {
1813 let option = setup_option(
1814 Side::Long,
1815 OptionStyle::Put,
1816 Positive::HUNDRED,
1817 pos_or_panic!(105.0),
1818 Positive::ONE,
1819 pos_or_panic!(30.0),
1820 );
1821 let position = Position::new(
1822 option,
1823 pos_or_panic!(5.0),
1824 Utc::now(),
1825 Positive::ONE,
1826 Positive::ONE,
1827 None,
1828 None,
1829 );
1830 assert_eq!(position.break_even().unwrap(), 93.0);
1831 }
1832
1833 #[test]
1834 fn test_unrealized_pnl_long_put_size() {
1835 let option = setup_option(
1836 Side::Long,
1837 OptionStyle::Put,
1838 Positive::HUNDRED,
1839 pos_or_panic!(105.0),
1840 pos_or_panic!(10.0),
1841 pos_or_panic!(30.0),
1842 );
1843 let position = Position::new(
1844 option,
1845 pos_or_panic!(5.0),
1846 Utc::now(),
1847 Positive::ONE,
1848 Positive::ONE,
1849 None,
1850 None,
1851 );
1852 assert_eq!(position.break_even().unwrap(), 93.0);
1853 }
1854
1855 #[test]
1856 fn test_unrealized_pnl_short_put() {
1857 let option = setup_option(
1858 Side::Short,
1859 OptionStyle::Put,
1860 Positive::HUNDRED,
1861 pos_or_panic!(105.0),
1862 Positive::ONE,
1863 pos_or_panic!(30.0),
1864 );
1865 let position = Position::new(
1866 option,
1867 pos_or_panic!(5.0),
1868 Utc::now(),
1869 Positive::ONE,
1870 Positive::ONE,
1871 None,
1872 None,
1873 );
1874 assert_eq!(position.break_even().unwrap(), 97.0);
1875 }
1876
1877 #[test]
1878 fn test_unrealized_pnl_short_put_size() {
1879 let option = setup_option(
1880 Side::Short,
1881 OptionStyle::Put,
1882 Positive::HUNDRED,
1883 pos_or_panic!(105.0),
1884 pos_or_panic!(10.0),
1885 pos_or_panic!(30.0),
1886 );
1887 let position = Position::new(
1888 option,
1889 pos_or_panic!(5.0),
1890 Utc::now(),
1891 Positive::ONE,
1892 Positive::ONE,
1893 None,
1894 None,
1895 );
1896 assert_eq!(position.break_even().unwrap(), 97.0);
1897 }
1898
1899 #[test]
1900 fn test_zero_quantity() {
1901 let option = setup_option(
1902 Side::Short,
1903 OptionStyle::Put,
1904 Positive::HUNDRED,
1905 pos_or_panic!(105.0),
1906 Positive::ZERO, pos_or_panic!(30.0),
1908 );
1909 let position = Position::new(
1910 option,
1911 pos_or_panic!(5.0),
1912 Utc::now(),
1913 Positive::ONE,
1914 Positive::ONE,
1915 None,
1916 None,
1917 );
1918 assert!(position.break_even().is_none());
1919 }
1920}
1921
1922#[cfg(test)]
1923mod tests_position_max_loss_profit {
1924 use super::*;
1925
1926 use crate::model::types::{OptionStyle, OptionType, Side};
1927
1928 use approx::assert_relative_eq;
1929 use positive::pos_or_panic;
1930 use rust_decimal_macros::dec;
1931
1932 fn setup_option(
1933 side: Side,
1934 option_style: OptionStyle,
1935 strike_price: Positive,
1936 underlying_price: Positive,
1937 quantity: Positive,
1938 expiration_days: Positive,
1939 ) -> Options {
1940 Options {
1941 option_type: OptionType::European,
1942 side,
1943 underlying_symbol: "".to_string(),
1944 strike_price,
1945 expiration_date: ExpirationDate::Days(expiration_days),
1946 implied_volatility: pos_or_panic!(0.2),
1947 quantity,
1948 contract_size: Positive::ONE,
1949 underlying_price,
1950 risk_free_rate: dec!(0.01),
1951 option_style,
1952 dividend_yield: Positive::ZERO,
1953 exotic_params: None,
1954 }
1955 }
1956
1957 #[test]
1958 fn test_unrealized_pnl_long_call() {
1959 let option = setup_option(
1960 Side::Long,
1961 OptionStyle::Call,
1962 Positive::HUNDRED,
1963 pos_or_panic!(105.0),
1964 Positive::ONE,
1965 pos_or_panic!(30.0),
1966 );
1967 let position = Position::new(
1968 option,
1969 pos_or_panic!(5.0),
1970 Utc::now(),
1971 Positive::ONE,
1972 Positive::ONE,
1973 None,
1974 None,
1975 );
1976 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 7.0, epsilon = 0.001);
1977 assert_eq!(position.max_profit().unwrap(), Positive::MAX);
1978 }
1979
1980 #[test]
1981 fn test_unrealized_pnl_long_call_size() {
1982 let option = setup_option(
1983 Side::Long,
1984 OptionStyle::Call,
1985 Positive::HUNDRED,
1986 pos_or_panic!(105.0),
1987 pos_or_panic!(10.0),
1988 pos_or_panic!(30.0),
1989 );
1990 let position = Position::new(
1991 option,
1992 pos_or_panic!(5.0),
1993 Utc::now(),
1994 Positive::ONE,
1995 Positive::ONE,
1996 None,
1997 None,
1998 );
1999 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 70.0, epsilon = 0.001);
2000 assert_eq!(position.max_profit().unwrap(), Positive::MAX);
2001 }
2002
2003 #[test]
2004 fn test_unrealized_pnl_short_call() {
2005 let option = setup_option(
2006 Side::Short,
2007 OptionStyle::Call,
2008 Positive::HUNDRED,
2009 pos_or_panic!(105.0),
2010 Positive::ONE,
2011 pos_or_panic!(30.0),
2012 );
2013 let position = Position::new(
2014 option,
2015 pos_or_panic!(5.0),
2016 Utc::now(),
2017 Positive::ONE,
2018 Positive::ONE,
2019 None,
2020 None,
2021 );
2022 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2023 assert_relative_eq!(
2024 position.max_profit().unwrap().to_f64(),
2025 3.0,
2026 epsilon = 0.001
2027 );
2028 }
2029
2030 #[test]
2031 fn test_unrealized_pnl_short_call_size() {
2032 let option = setup_option(
2033 Side::Short,
2034 OptionStyle::Call,
2035 Positive::HUNDRED,
2036 pos_or_panic!(105.0),
2037 pos_or_panic!(10.0),
2038 pos_or_panic!(30.0),
2039 );
2040 let position = Position::new(
2041 option,
2042 pos_or_panic!(5.0),
2043 Utc::now(),
2044 Positive::ONE,
2045 Positive::ONE,
2046 None,
2047 None,
2048 );
2049 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2050 assert_relative_eq!(
2051 position.max_profit().unwrap().to_f64(),
2052 30.0,
2053 epsilon = 0.001
2054 );
2055 }
2056
2057 #[test]
2058 fn test_unrealized_pnl_long_put() {
2059 let option = setup_option(
2060 Side::Long,
2061 OptionStyle::Put,
2062 Positive::HUNDRED,
2063 pos_or_panic!(105.0),
2064 Positive::ONE,
2065 pos_or_panic!(30.0),
2066 );
2067 let position = Position::new(
2068 option,
2069 pos_or_panic!(5.0),
2070 Utc::now(),
2071 Positive::ONE,
2072 Positive::ONE,
2073 None,
2074 None,
2075 );
2076 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 7.0, epsilon = 0.001);
2077 assert_relative_eq!(position.max_profit().unwrap(), Positive::MAX);
2078 }
2079
2080 #[test]
2081 fn test_unrealized_pnl_long_put_size() {
2082 let option = setup_option(
2083 Side::Long,
2084 OptionStyle::Put,
2085 Positive::HUNDRED,
2086 pos_or_panic!(105.0),
2087 pos_or_panic!(10.0),
2088 pos_or_panic!(30.0),
2089 );
2090 let position = Position::new(
2091 option,
2092 pos_or_panic!(5.0),
2093 Utc::now(),
2094 Positive::ONE,
2095 Positive::ONE,
2096 None,
2097 None,
2098 );
2099 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 70.0, epsilon = 0.001);
2100 assert_relative_eq!(position.max_profit().unwrap(), Positive::MAX);
2101 }
2102
2103 #[test]
2104 fn test_unrealized_pnl_short_put() {
2105 let option = setup_option(
2106 Side::Short,
2107 OptionStyle::Put,
2108 Positive::HUNDRED,
2109 pos_or_panic!(105.0),
2110 Positive::ONE,
2111 pos_or_panic!(30.0),
2112 );
2113 let position = Position::new(
2114 option,
2115 pos_or_panic!(5.0),
2116 Utc::now(),
2117 Positive::ONE,
2118 Positive::ONE,
2119 None,
2120 None,
2121 );
2122 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2123 assert_relative_eq!(
2124 position.max_profit().unwrap().to_f64(),
2125 3.0,
2126 epsilon = 0.001
2127 );
2128 }
2129
2130 #[test]
2131 fn test_unrealized_pnl_short_put_size() {
2132 let option = setup_option(
2133 Side::Short,
2134 OptionStyle::Put,
2135 Positive::HUNDRED,
2136 pos_or_panic!(105.0),
2137 pos_or_panic!(10.0),
2138 pos_or_panic!(30.0),
2139 );
2140 let position = Position::new(
2141 option,
2142 pos_or_panic!(5.0),
2143 Utc::now(),
2144 Positive::ONE,
2145 Positive::ONE,
2146 None,
2147 None,
2148 );
2149 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2150 assert_relative_eq!(
2151 position.max_profit().unwrap().to_f64(),
2152 30.0,
2153 epsilon = 0.001
2154 );
2155 }
2156}
2157
2158#[cfg(test)]
2159mod tests_premium {
2160 use super::*;
2161 use positive::pos_or_panic;
2162
2163 fn setup_basic_position(side: Side) -> Position {
2164 let option = Options {
2165 side,
2166 quantity: Positive::ONE,
2167 ..Default::default()
2168 };
2169
2170 Position::new(
2171 option,
2172 pos_or_panic!(5.0),
2173 Utc::now(),
2174 Positive::ONE,
2175 Positive::ONE,
2176 None,
2177 None,
2178 )
2179 }
2180
2181 #[test]
2182 fn test_premium_received_long() {
2183 let position = setup_basic_position(Side::Long);
2184 assert_eq!(position.premium_received().unwrap(), Positive::ZERO);
2185 }
2186
2187 #[test]
2188 fn test_premium_received_short() {
2189 let position = setup_basic_position(Side::Short);
2190 assert_eq!(position.premium_received().unwrap(), 5.0);
2191 }
2192
2193 #[test]
2194 fn test_net_premium_received_long() {
2195 let position = setup_basic_position(Side::Long);
2196 assert_eq!(position.net_premium_received().unwrap(), 0.0);
2197 }
2198
2199 #[test]
2200 fn test_net_premium_received_short() {
2201 let position = setup_basic_position(Side::Short);
2202 assert_eq!(position.net_premium_received().unwrap(), 3.0); }
2204
2205 #[test]
2206 fn test_premium_received_with_quantity() {
2207 let side = Side::Short;
2208 let option = Options {
2209 side,
2210 quantity: pos_or_panic!(10.0),
2211 ..Default::default()
2212 };
2213
2214 let position = Position::new(
2215 option,
2216 pos_or_panic!(5.0),
2217 Utc::now(),
2218 Positive::ONE,
2219 Positive::ONE,
2220 None,
2221 None,
2222 );
2223 assert_eq!(position.premium_received().unwrap(), 50.0);
2224 }
2225}
2226
2227#[cfg(test)]
2228mod tests_position_serde {
2229 use super::*;
2230
2231 use crate::model::utils::create_sample_position;
2232
2233 use positive::pos_or_panic;
2234 use serde_json;
2235 use tracing::info;
2236
2237 #[test]
2238 fn test_position_serialization() {
2239 let position = create_sample_position(
2240 OptionStyle::Call,
2241 Side::Short,
2242 pos_or_panic!(90.0),
2243 Positive::ONE,
2244 pos_or_panic!(95.0),
2245 pos_or_panic!(0.2),
2246 );
2247 let serialized = serde_json::to_string(&position).unwrap();
2248
2249 assert!(serialized.contains("\"option\""));
2251 assert!(serialized.contains("\"premium\""));
2252 assert!(serialized.contains("\"date\""));
2253 assert!(serialized.contains("\"open_fee\""));
2254 assert!(serialized.contains("\"close_fee\""));
2255 assert!(serialized.contains("AAPL"));
2256 assert!(serialized.contains("95"));
2257 }
2258
2259 #[test]
2260 fn test_position_deserialization() {
2261 let position = create_sample_position(
2262 OptionStyle::Call,
2263 Side::Short,
2264 pos_or_panic!(90.0),
2265 Positive::ONE,
2266 pos_or_panic!(95.0),
2267 pos_or_panic!(0.2),
2268 );
2269 let serialized = serde_json::to_string(&position).unwrap();
2270 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2271
2272 assert_eq!(position, deserialized);
2273 assert_eq!(deserialized.option.underlying_symbol, "AAPL");
2274 assert_eq!(deserialized.option.strike_price, pos_or_panic!(95.0));
2275 assert_eq!(deserialized.premium, pos_or_panic!(5.0));
2276 assert_eq!(deserialized.open_fee, pos_or_panic!(0.5));
2277 assert_eq!(deserialized.close_fee, pos_or_panic!(0.5));
2278 }
2279
2280 #[test]
2281 fn test_position_json_structure() {
2282 let position = create_sample_position(
2283 OptionStyle::Call,
2284 Side::Short,
2285 pos_or_panic!(90.0),
2286 Positive::ONE,
2287 pos_or_panic!(95.0),
2288 pos_or_panic!(0.2),
2289 );
2290 let serialized = serde_json::to_string_pretty(&position).unwrap();
2291
2292 info!("Serialized Position:\n{}", serialized);
2293
2294 let value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2295
2296 assert!(value.is_object());
2298 assert!(value.get("option").is_some());
2299 assert!(value.get("premium").is_some());
2300 assert!(value.get("date").is_some());
2301 assert!(value.get("open_fee").is_some());
2302 assert!(value.get("close_fee").is_some());
2303 }
2304
2305 #[test]
2306 fn test_position_deserialize_invalid_json() {
2307 let invalid_json = r#"{
2308 "option": null,
2309 "premium": 5.0,
2310 "date": "2024-01-01T00:00:00Z",
2311 "open_fee": 1.0,
2312 "close_fee": 1.0
2313 }"#;
2314
2315 let result: Result<Position, serde_json::Error> = serde_json::from_str(invalid_json);
2316 assert!(result.is_err());
2317 }
2318
2319 #[test]
2320 fn test_position_roundtrip() {
2321 let original = create_sample_position(
2322 OptionStyle::Call,
2323 Side::Short,
2324 pos_or_panic!(90.0),
2325 Positive::ONE,
2326 pos_or_panic!(95.0),
2327 pos_or_panic!(0.2),
2328 );
2329 let serialized = serde_json::to_string(&original).unwrap();
2330 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2331
2332 assert_eq!(original, deserialized);
2334
2335 let reserialized = serde_json::to_string(&deserialized).unwrap();
2337 let redeserialized: Position = serde_json::from_str(&reserialized).unwrap();
2338 assert_eq!(deserialized, redeserialized);
2339 }
2340
2341 #[test]
2342 fn test_position_with_different_option_types() {
2343 let put_position = create_sample_position(
2345 OptionStyle::Put,
2346 Side::Short,
2347 pos_or_panic!(90.0),
2348 Positive::ONE,
2349 pos_or_panic!(95.0),
2350 pos_or_panic!(0.2),
2351 );
2352
2353 let serialized = serde_json::to_string(&put_position).unwrap();
2354 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2355
2356 assert_eq!(put_position, deserialized);
2357 assert_eq!(deserialized.option.option_style, OptionStyle::Put);
2358
2359 let short_position = create_sample_position(
2361 OptionStyle::Call,
2362 Side::Short,
2363 pos_or_panic!(90.0),
2364 Positive::ONE,
2365 pos_or_panic!(95.0),
2366 pos_or_panic!(0.2),
2367 );
2368
2369 let serialized = serde_json::to_string(&short_position).unwrap();
2370 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2371
2372 assert_eq!(short_position, deserialized);
2373 assert_eq!(deserialized.option.side, Side::Short);
2374 }
2375}
2376
2377#[cfg(test)]
2378mod tests_position_tradeable_trait {
2379 use super::*;
2380
2381 use crate::model::utils::create_sample_position;
2382 use positive::pos_or_panic;
2383
2384 #[test]
2385 fn test_tradeable_trade_ref() {
2386 let position = create_sample_position(
2387 OptionStyle::Call,
2388 Side::Short,
2389 pos_or_panic!(90.0),
2390 Positive::ONE,
2391 pos_or_panic!(95.0),
2392 pos_or_panic!(0.2),
2393 );
2394
2395 let result = position.trade_ref();
2396 assert!(result.is_err());
2397 }
2398
2399 #[test]
2400 fn test_tradeable_trade_mut() {
2401 let mut position = create_sample_position(
2402 OptionStyle::Call,
2403 Side::Short,
2404 pos_or_panic!(90.0),
2405 Positive::ONE,
2406 pos_or_panic!(95.0),
2407 pos_or_panic!(0.2),
2408 );
2409
2410 let result = position.trade_mut();
2411 assert!(result.is_err());
2412 }
2413}
2414
2415#[cfg(test)]
2416mod tests_position_tradestatusable_trait {
2417 use super::*;
2418
2419 use crate::model::utils::create_sample_position;
2420 use positive::pos_or_panic;
2421
2422 #[test]
2423 fn test_tradestatusable_expired() {
2424 let position = create_sample_position(
2425 OptionStyle::Call,
2426 Side::Short,
2427 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2432
2433 let result = position.expired();
2434 assert!(result.is_ok());
2435 }
2436
2437 #[test]
2438 fn test_tradestatusable_exercised() {
2439 let position = create_sample_position(
2440 OptionStyle::Call,
2441 Side::Short,
2442 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2447
2448 let result = position.exercised();
2449 assert!(result.is_ok());
2450 }
2451
2452 #[test]
2453 fn test_tradestatusable_assigned() {
2454 let position = create_sample_position(
2455 OptionStyle::Call,
2456 Side::Short,
2457 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2462
2463 let result = position.assigned();
2464 assert!(result.is_ok());
2465 }
2466
2467 #[test]
2468 fn test_tradestatusable_status_other() {
2469 let position = create_sample_position(
2470 OptionStyle::Call,
2471 Side::Short,
2472 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2477
2478 let result = position.status_other();
2479 assert!(result.is_ok());
2480 }
2481
2482 #[test]
2488 fn test_tradestatusable_close_zeros_tiny_premium() {
2489 use crate::model::utils::create_sample_option_simplest;
2490
2491 let build = |premium: Positive| {
2492 Position::new(
2493 create_sample_option_simplest(OptionStyle::Call, Side::Long),
2494 premium,
2495 Utc::now(),
2496 pos_or_panic!(0.01),
2497 pos_or_panic!(0.01),
2498 None,
2499 None,
2500 )
2501 };
2502
2503 let position = build(pos_or_panic!(0.02));
2505 let trade = position.close().expect("close() should succeed");
2506 assert_eq!(trade.premium, pos_or_panic!(0.02));
2507
2508 let position = build(pos_or_panic!(0.01));
2510 let trade = position.close().expect("close() should succeed");
2511 assert_eq!(trade.premium, Positive::ZERO);
2512
2513 let position = build(pos_or_panic!(0.005));
2515 let trade = position.close().expect("close() should succeed");
2516 assert_eq!(trade.premium, Positive::ZERO);
2517 }
2518}
2519
2520#[cfg(test)]
2521mod tests_position_contract_size {
2522 use super::*;
2523 use crate::model::leg::{Leg, LegAble};
2524 use crate::model::utils::create_sample_option;
2525 use positive::pos_or_panic;
2526 use rust_decimal_macros::dec;
2527
2528 fn sized_call(side: Side, contract_size: Positive) -> Position {
2531 let option = create_sample_option(
2532 OptionStyle::Call,
2533 side,
2534 Positive::HUNDRED,
2535 Positive::TWO,
2536 Positive::HUNDRED,
2537 pos_or_panic!(0.2),
2538 )
2539 .with_contract_size(contract_size);
2540 Position::new(
2541 option,
2542 pos_or_panic!(5.0),
2543 Utc::now(),
2544 pos_or_panic!(0.5),
2545 pos_or_panic!(0.5),
2546 None,
2547 None,
2548 )
2549 }
2550
2551 #[test]
2552 fn test_position_contract_size_fees_stay_per_contract() {
2553 let long = sized_call(Side::Long, Positive::HUNDRED);
2554 assert_eq!(long.fees().ok(), Some(Positive::TWO));
2555 let short = sized_call(Side::Short, Positive::HUNDRED);
2556 assert_eq!(short.fees().ok(), Some(Positive::TWO));
2557 }
2558
2559 #[test]
2560 fn test_position_contract_size_long_cost_scales_premium_not_fees() {
2561 let long = sized_call(Side::Long, Positive::HUNDRED);
2563 assert_eq!(long.total_cost().ok(), Some(pos_or_panic!(1002.0)));
2564 assert_eq!(long.premium_received().ok(), Some(Positive::ZERO));
2565 assert_eq!(long.net_cost().ok(), Some(dec!(1002)));
2566 }
2567
2568 #[test]
2569 fn test_position_contract_size_short_premium_scales_fees_do_not() {
2570 let short = sized_call(Side::Short, Positive::HUNDRED);
2572 assert_eq!(short.total_cost().ok(), Some(Positive::TWO));
2573 assert_eq!(short.premium_received().ok(), Some(pos_or_panic!(1000.0)));
2574 assert_eq!(
2575 short.net_premium_received().ok(),
2576 Some(pos_or_panic!(998.0))
2577 );
2578 assert_eq!(short.net_cost().ok(), Some(dec!(-998)));
2579 }
2580
2581 #[test]
2582 fn test_position_contract_size_pnl_at_expiration_scales() {
2583 let long = sized_call(Side::Long, Positive::HUNDRED);
2585 assert_eq!(
2586 long.pnl_at_expiration(&Some(&pos_or_panic!(120.0))).ok(),
2587 Some(dec!(2998))
2588 );
2589 assert_eq!(
2590 long.pnl_at_expiration(&Some(&pos_or_panic!(90.0))).ok(),
2591 Some(dec!(-1002))
2592 );
2593 let short = sized_call(Side::Short, Positive::HUNDRED);
2595 assert_eq!(
2596 short.pnl_at_expiration(&Some(&pos_or_panic!(110.0))).ok(),
2597 Some(dec!(-1002))
2598 );
2599 assert_eq!(
2600 short.pnl_at_expiration(&Some(&pos_or_panic!(90.0))).ok(),
2601 Some(dec!(998))
2602 );
2603 }
2604
2605 #[test]
2606 fn test_position_contract_size_unrealized_pnl_scales_price_not_fees() {
2607 let long = sized_call(Side::Long, Positive::HUNDRED);
2609 assert_eq!(
2610 long.unrealized_pnl(pos_or_panic!(7.0)).ok(),
2611 Some(dec!(398))
2612 );
2613 let short = sized_call(Side::Short, Positive::HUNDRED);
2614 assert_eq!(
2615 short.unrealized_pnl(pos_or_panic!(3.0)).ok(),
2616 Some(dec!(398))
2617 );
2618 }
2619
2620 #[test]
2621 fn test_position_contract_size_break_even_spreads_fees_over_units() {
2622 let long = sized_call(Side::Long, Positive::HUNDRED);
2624 assert_eq!(long.break_even(), Some(pos_or_panic!(105.01)));
2625 let short = sized_call(Side::Short, Positive::HUNDRED);
2626 assert_eq!(short.break_even(), Some(pos_or_panic!(104.99)));
2627 for position in [long, short] {
2629 let Some(break_even) = position.break_even() else {
2630 panic!("break-even exists");
2631 };
2632 assert_eq!(
2633 position.pnl_at_expiration(&Some(&break_even)).ok(),
2634 Some(Decimal::ZERO)
2635 );
2636 }
2637 }
2638
2639 #[test]
2640 fn test_position_contract_size_one_matches_default() {
2641 let default = sized_call(Side::Long, Positive::ONE);
2642 let mut legacy = default.clone();
2643 legacy.option = create_sample_option(
2644 OptionStyle::Call,
2645 Side::Long,
2646 Positive::HUNDRED,
2647 Positive::TWO,
2648 Positive::HUNDRED,
2649 pos_or_panic!(0.2),
2650 );
2651 assert_eq!(default, legacy);
2652 assert_eq!(default.total_cost().ok(), Some(pos_or_panic!(12.0)));
2653 }
2654
2655 #[test]
2656 fn test_position_contract_size_trade_carries_contract_size() {
2657 let long = sized_call(Side::Long, Positive::HUNDRED);
2658 let Ok(trade) = long.trade() else {
2659 panic!("trade builds");
2660 };
2661 assert_eq!(trade.premium, pos_or_panic!(5.0));
2664 assert_eq!(trade.contract_size, Positive::HUNDRED);
2665 assert_eq!(trade.fee, Positive::ONE);
2666 assert_eq!(trade.quantity, Positive::TWO);
2667 assert_eq!(trade.cost().unwrap(), pos_or_panic!(1002.0));
2669 assert_eq!(trade.income().unwrap(), Positive::ZERO);
2670 assert_eq!(trade.net().unwrap(), dec!(-1002));
2671 let Ok(sold) = sized_call(Side::Short, Positive::HUNDRED).trade() else {
2672 panic!("trade builds");
2673 };
2674 let mut sold = sold;
2675 sold.action = Action::Sell;
2676 assert_eq!(sold.cost().unwrap(), pos_or_panic!(1002.0));
2678 }
2679
2680 #[test]
2681 fn test_position_contract_size_trade_matches_unit_lots() {
2682 let Ok(sized) = sized_call(Side::Long, Positive::HUNDRED).trade() else {
2685 panic!("trade builds");
2686 };
2687 let mut lots = sized.clone();
2688 lots.contract_size = Positive::ONE;
2689 lots.quantity = pos_or_panic!(200.0);
2690 lots.fee = pos_or_panic!(0.01);
2691 assert_eq!(sized.cost().unwrap(), lots.cost().unwrap());
2692 assert_eq!(sized.net().unwrap(), lots.net().unwrap());
2693 }
2694
2695 #[test]
2696 fn test_position_contract_size_trade_serde_round_trip() {
2697 let Ok(trade) = sized_call(Side::Long, Positive::HUNDRED).trade() else {
2698 panic!("trade builds");
2699 };
2700 let Ok(json) = serde_json::to_string(&trade) else {
2701 panic!("trade serializes");
2702 };
2703 let Ok(back) = serde_json::from_str::<Trade>(&json) else {
2704 panic!("trade deserializes");
2705 };
2706 assert_eq!(back.contract_size, Positive::HUNDRED);
2707 assert_eq!(back, trade);
2708 }
2709
2710 #[test]
2711 fn test_position_contract_size_leg_notional_covers_all_units() {
2712 let leg = Leg::from(sized_call(Side::Long, Positive::HUNDRED));
2713 assert_eq!(leg.get_quantity(), Positive::TWO);
2714 assert_eq!(
2715 leg.notional_value(Positive::HUNDRED).ok(),
2716 Some(pos_or_panic!(20000.0))
2717 );
2718 assert_eq!(
2719 leg.pnl_at_price(pos_or_panic!(120.0)).ok(),
2720 Some(dec!(2998))
2721 );
2722 }
2723}