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};
15#[cfg(feature = "clock")]
16use crate::utils::time::wall_clock;
17use crate::utils::time::{evaluation_time, now};
18use chrono::{DateTime, Utc};
19use positive::Positive;
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22use tracing::debug;
23
24#[derive(Clone, PartialEq, Serialize, Deserialize)]
77#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
78pub struct Position {
79 pub option: Options,
82
83 pub premium: Positive,
89
90 pub date: DateTime<Utc>,
93
94 pub open_fee: Positive,
98
99 pub close_fee: Positive,
103
104 pub epic: Option<String>,
106
107 pub extra_fields: Option<serde_json::Value>,
109}
110
111impl Position {
112 #[inline]
122 fn premium_per_contract(&self) -> Result<Positive, PositionError> {
123 Ok(self.premium.checked_mul(&self.option.contract_size)?)
124 }
125
126 #[must_use]
175 pub fn new(
176 option: Options,
177 premium: Positive,
178 date: DateTime<Utc>,
179 open_fee: Positive,
180 close_fee: Positive,
181 epic: Option<String>,
182 extra_fields: Option<serde_json::Value>,
183 ) -> Self {
184 Position {
185 option,
186 premium,
187 date,
188 open_fee,
189 close_fee,
190 epic,
191 extra_fields,
192 }
193 }
194
195 pub fn total_cost(&self) -> Result<Positive, PositionError> {
220 let total_cost = match self.option.side {
221 Side::Long => self
222 .premium_per_contract()?
223 .checked_add(&self.open_fee)?
224 .checked_add(&self.close_fee)?
225 .checked_mul(&self.option.quantity)?,
226 Side::Short => self.fees()?,
227 };
228
229 Ok(total_cost)
230 }
231
232 pub fn premium_received(&self) -> Result<Positive, PositionError> {
285 match self.option.side {
286 Side::Long => Ok(Positive::ZERO),
287 Side::Short => Ok(self
288 .premium_per_contract()?
289 .checked_mul(&self.option.quantity)?),
290 }
291 }
292
293 pub fn net_premium_received(&self) -> Result<Positive, PositionError> {
322 match self.option.side {
323 Side::Long => Ok(Positive::ZERO),
324 Side::Short => {
325 let premium = self
327 .premium_per_contract()?
328 .checked_mul(&self.option.quantity)?;
329 let total_cost = self.total_cost()?;
330 if premium >= total_cost {
331 Ok(premium.checked_sub(&total_cost)?)
332 } else {
333 Ok(Positive::ZERO)
334 }
335 }
336 }
337 }
338
339 pub fn pnl_at_expiration(&self, price: &Option<&Positive>) -> Result<Decimal, PositionError> {
396 let intrinsic = match price {
402 None => self.option.intrinsic_value(self.option.underlying_price)?,
403 Some(price) => self.option.intrinsic_value(**price)?,
404 };
405 let cost = self.total_cost()?.to_dec();
406 let premium_recv = self.premium_received()?.to_dec();
407 let net_after_cost = d_sub(intrinsic, cost, "position::pnl_at_expiration::net")?;
408 d_add(
409 net_after_cost,
410 premium_recv,
411 "position::pnl_at_expiration::total",
412 )
413 .map_err(PositionError::from)
414 }
415
416 pub fn unrealized_pnl(&self, price: Positive) -> Result<Decimal, PositionError> {
475 let per_contract = match self.option.side {
482 Side::Long => {
483 let after_premium = d_mul(
484 d_sub(
485 price.to_dec(),
486 self.premium.to_dec(),
487 "position::unrealized_pnl::long::after_premium",
488 )?,
489 self.option.contract_size.to_dec(),
490 "position::unrealized_pnl::long::contract_size",
491 )?;
492 let after_open = d_sub(
493 after_premium,
494 self.open_fee.to_dec(),
495 "position::unrealized_pnl::long::after_open_fee",
496 )?;
497 d_sub(
498 after_open,
499 self.close_fee.to_dec(),
500 "position::unrealized_pnl::long::net",
501 )?
502 }
503 Side::Short => {
504 let after_price = d_mul(
505 d_sub(
506 self.premium.to_dec(),
507 price.to_dec(),
508 "position::unrealized_pnl::short::after_price",
509 )?,
510 self.option.contract_size.to_dec(),
511 "position::unrealized_pnl::short::contract_size",
512 )?;
513 let after_open = d_sub(
514 after_price,
515 self.open_fee.to_dec(),
516 "position::unrealized_pnl::short::after_open_fee",
517 )?;
518 d_sub(
519 after_open,
520 self.close_fee.to_dec(),
521 "position::unrealized_pnl::short::net",
522 )?
523 }
524 };
525 Ok(d_mul(
526 per_contract,
527 self.option.quantity.to_dec(),
528 "position::unrealized_pnl::scaled",
529 )?)
530 }
531
532 pub fn days_held(&self) -> Result<Positive, PositionError> {
554 self.days_held_at(evaluation_time()?)
555 }
556
557 pub fn days_held_at(&self, at: DateTime<Utc>) -> Result<Positive, PositionError> {
568 let days = at.signed_duration_since(self.date).num_days() as f64;
571 Positive::new(days).map_err(|e| {
572 PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
573 reason: format!("failed to calculate days held: {}", e),
574 })
575 })
576 }
577
578 pub fn days_to_expiration(&self) -> Result<Positive, PositionError> {
602 match self.option.expiration_date {
603 ExpirationDate::Days(days) => Ok(days),
604 ExpirationDate::DateTime(_) => self.days_to_expiration_at(evaluation_time()?),
605 }
606 }
607
608 pub fn days_to_expiration_at(&self, at: DateTime<Utc>) -> Result<Positive, PositionError> {
618 match self.option.expiration_date {
619 ExpirationDate::Days(days) => Ok(days),
620 ExpirationDate::DateTime(datetime) => {
621 let days = datetime.signed_duration_since(at).num_days() as f64;
622 Positive::new(days.max(0.0)).map_err(|e| {
623 PositionError::ValidationError(PositionValidationErrorKind::InvalidPosition {
624 reason: format!("failed to calculate days to expiration: {}", e),
625 })
626 })
627 }
628 }
629 }
630
631 #[must_use]
636 pub fn blank_at(date: DateTime<Utc>) -> Self {
637 Position {
638 option: Options::default(),
639 premium: Positive::ZERO,
640 date,
641 open_fee: Positive::ZERO,
642 close_fee: Positive::ZERO,
643 epic: None,
644 extra_fields: None,
645 }
646 }
647
648 pub fn trade_at(&self, at: DateTime<Utc>) -> Result<Trade, TradeError> {
659 self.build_trade(self.option.expiration_date.get_date_at(at).ok(), at)
660 }
661
662 fn build_trade(
664 &self,
665 expiry: Option<DateTime<Utc>>,
666 at: DateTime<Utc>,
667 ) -> Result<Trade, TradeError> {
668 let fee = self
669 .open_fee
670 .checked_add(&self.close_fee)
671 .map_err(|error| {
672 TradeError::invalid_trade(&format!(
673 "open and close fees overflow the Positive range: {error}"
674 ))
675 })?;
676 self.premium_per_contract().map_err(|error| {
681 TradeError::invalid_trade(&format!(
682 "premium times contract size overflows the Positive range: {error}"
683 ))
684 })?;
685 if let (Some(expiry), Some(timestamp)) = (expiry, at.timestamp_nanos_opt()) {
686 Ok(Trade {
687 id: uuid::Uuid::new_v4(),
688 action: Action::Buy,
689 side: self.option.side,
690 option_style: self.option.option_style,
691 fee,
692 symbol: None,
693 strike: self.option.strike_price,
694 expiry,
695 timestamp,
696 quantity: self.option.quantity,
697 premium: self.premium,
698 contract_size: self.option.contract_size,
699 underlying_price: self.option.underlying_price,
700 notes: None,
701 status: TradeStatus::Other("Not yet initialized".to_string()),
702 })
703 } else {
704 Err(TradeError::invalid_trade(
705 "Could not create trade from position",
706 ))
707 }
708 }
709
710 #[must_use]
721 pub fn is_long(&self) -> bool {
722 match self.option.side {
723 Side::Long => true,
724 Side::Short => false,
725 }
726 }
727
728 #[must_use]
739 pub fn is_short(&self) -> bool {
740 match self.option.side {
741 Side::Long => false,
742 Side::Short => true,
743 }
744 }
745
746 pub fn net_cost(&self) -> Result<Decimal, PositionError> {
767 match self.option.side {
768 Side::Long => Ok(self.total_cost()?.to_dec()),
769 Side::Short => {
770 let fees = self.fees()?.to_dec();
771 let premium = self.premium_received()?.to_dec();
772 Ok(d_sub(fees, premium, "Position::net_cost/short")?)
773 }
774 }
775 }
776
777 #[must_use]
806 pub fn break_even(&self) -> Option<Positive> {
807 if self.option.quantity == Positive::ZERO {
808 return None;
809 }
810 let position_total_cost = self.total_cost().ok()?;
811 let position_size = self.option.position_size().ok()?;
815 let total_cost_per_unit = position_total_cost.checked_div(&position_size).ok()?;
816 match (&self.option.side, &self.option.option_style) {
817 (Side::Long, OptionStyle::Call) => self
818 .option
819 .strike_price
820 .checked_add(&total_cost_per_unit)
821 .ok(),
822 (Side::Short, OptionStyle::Call) => self
823 .option
824 .strike_price
825 .checked_add(&self.premium)
826 .and_then(|total| total.checked_sub(&total_cost_per_unit))
827 .ok(),
828 (Side::Long, OptionStyle::Put) => self
829 .option
830 .strike_price
831 .checked_sub(&total_cost_per_unit)
832 .ok(),
833 (Side::Short, OptionStyle::Put) => self
834 .option
835 .strike_price
836 .checked_sub(&self.premium)
837 .and_then(|net| net.checked_add(&total_cost_per_unit))
838 .ok(),
839 }
840 }
841
842 #[allow(dead_code)]
855 pub(crate) fn max_profit(&self) -> Result<Positive, PositionError> {
856 match self.option.side {
857 Side::Long => Ok(Positive::MAX),
858 Side::Short => self.net_premium_received(),
859 }
860 }
861
862 #[allow(dead_code)]
875 pub(crate) fn max_loss(&self) -> Result<Positive, PositionError> {
876 match self.option.side {
877 Side::Long => self.total_cost(),
878 Side::Short => Ok(Positive::MAX),
879 }
880 }
881
882 pub fn fees(&self) -> Result<Positive, PositionError> {
900 Ok(self
901 .open_fee
902 .checked_add(&self.close_fee)?
903 .checked_mul(&self.option.quantity)?)
904 }
905
906 pub fn validate(&self) -> bool {
945 if self.option.side == Side::Short && self.premium == Positive::ZERO {
946 debug!("Premium must be greater than zero for short positions.");
947 return false;
948 }
949 if !self.option.validate() {
950 debug!("Option is not valid.");
951 return false;
952 }
953 true
954 }
955}
956
957#[cfg(feature = "clock")]
958impl Default for Position {
959 fn default() -> Self {
960 Self::blank_at(wall_clock())
961 }
962}
963
964impl TradeAble for Position {
965 fn trade(&self) -> Result<Trade, TradeError> {
966 self.build_trade(
970 resolve_expiration_date(&self.option.expiration_date).ok(),
971 now()?,
972 )
973 }
974
975 fn trade_ref(&self) -> Result<&Trade, TradeError> {
976 Err(TradeError::invalid_trade(
977 "trade_ref() is not implemented for Position",
978 ))
979 }
980
981 fn trade_mut(&mut self) -> Result<&mut Trade, TradeError> {
982 Err(TradeError::invalid_trade(
983 "trade_mut() is not implemented for Position",
984 ))
985 }
986}
987
988impl TradeStatusAble for Position {
989 fn open(&self) -> Result<Trade, TradeError> {
990 let mut trade = self.trade()?;
991 trade.status = TradeStatus::Open;
992 Ok(trade)
993 }
994
995 fn close(&self) -> Result<Trade, TradeError> {
996 let mut trade = self.trade()?;
997 if trade.premium.to_dec() <= rust_decimal_macros::dec!(0.01) {
1003 trade.premium = Positive::ZERO;
1004 }
1005 trade.status = TradeStatus::Closed;
1006 trade.action = Action::Sell;
1007 Ok(trade)
1008 }
1009
1010 fn expired(&self) -> Result<Trade, TradeError> {
1011 let mut trade = self.trade()?;
1012 trade.status = TradeStatus::Expired;
1013 trade.action = Action::Sell;
1014 Ok(trade)
1015 }
1016
1017 fn exercised(&self) -> Result<Trade, TradeError> {
1018 let mut trade = self.trade()?;
1019 trade.status = TradeStatus::Exercised;
1020 trade.action = Action::Sell;
1021 Ok(trade)
1022 }
1023
1024 fn assigned(&self) -> Result<Trade, TradeError> {
1025 let mut trade = self.trade()?;
1026 trade.status = TradeStatus::Assigned;
1027 trade.action = Action::Other;
1028 Ok(trade)
1029 }
1030
1031 fn status_other(&self) -> Result<Trade, TradeError> {
1032 let mut trade = self.trade()?;
1033 trade.status = TradeStatus::Other("Not yet initialized".to_string());
1034 trade.action = Action::Other;
1035 Ok(trade)
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests_position {
1041 #[cfg(feature = "clock")]
1042 use super::*;
1043
1044 #[cfg(feature = "clock")]
1045 use crate::constants::ZERO;
1046 #[cfg(feature = "clock")]
1047 use crate::model::types::{OptionStyle, OptionType, Side};
1048
1049 #[cfg(feature = "clock")]
1050 use chrono::Duration;
1051 #[cfg(feature = "clock")]
1052 use num_traits::ToPrimitive;
1053 #[cfg(feature = "clock")]
1054 use positive::pos_or_panic;
1055 #[cfg(feature = "clock")]
1056 use rust_decimal_macros::dec;
1057
1058 #[cfg(feature = "clock")]
1059 fn setup_option(
1060 side: Side,
1061 option_style: OptionStyle,
1062 strike_price: Positive,
1063 underlying_price: Positive,
1064 quantity: Positive,
1065 expiration_days: Positive,
1066 ) -> Options {
1067 Options {
1068 option_type: OptionType::European,
1069 side,
1070 underlying_symbol: "".to_string(),
1071 strike_price,
1072 expiration_date: ExpirationDate::Days(expiration_days),
1073 implied_volatility: pos_or_panic!(0.2),
1074 quantity,
1075 contract_size: Positive::ONE,
1076 underlying_price,
1077 risk_free_rate: dec!(0.01),
1078 option_style,
1079 dividend_yield: Positive::ZERO,
1080 exotic_params: None,
1081 }
1082 }
1083
1084 #[cfg(feature = "clock")]
1085 #[test]
1086 fn test_position_total_cost() {
1087 let option = setup_option(
1088 Side::Long,
1089 OptionStyle::Call,
1090 Positive::HUNDRED,
1091 pos_or_panic!(105.0),
1092 Positive::ONE,
1093 pos_or_panic!(30.0),
1094 );
1095 let position = Position::new(
1096 option,
1097 pos_or_panic!(5.0),
1098 Utc::now(),
1099 Positive::ONE,
1100 Positive::ONE,
1101 None,
1102 None,
1103 );
1104 assert_eq!(
1105 position.total_cost().unwrap(),
1106 7.0,
1107 "Total cost calculation is incorrect."
1108 );
1109 }
1110
1111 #[cfg(feature = "clock")]
1112 #[test]
1113 fn test_position_total_cost_size() {
1114 let option = setup_option(
1115 Side::Long,
1116 OptionStyle::Call,
1117 Positive::HUNDRED,
1118 pos_or_panic!(105.0),
1119 pos_or_panic!(10.0),
1120 pos_or_panic!(30.0),
1121 );
1122 let position = Position::new(
1123 option,
1124 pos_or_panic!(5.0),
1125 Utc::now(),
1126 Positive::ONE,
1127 Positive::ONE,
1128 None,
1129 None,
1130 );
1131 assert_eq!(
1132 position.total_cost().unwrap(),
1133 70.0,
1134 "Total cost calculation is incorrect."
1135 );
1136 }
1137
1138 #[cfg(feature = "clock")]
1139 #[test]
1140 fn test_position_total_cost_short() {
1141 let option = setup_option(
1142 Side::Short,
1143 OptionStyle::Call,
1144 Positive::HUNDRED,
1145 pos_or_panic!(105.0),
1146 Positive::ONE,
1147 pos_or_panic!(30.0),
1148 );
1149 let position = Position::new(
1150 option,
1151 pos_or_panic!(5.0),
1152 Utc::now(),
1153 Positive::ONE,
1154 Positive::ONE,
1155 None,
1156 None,
1157 );
1158 assert_eq!(
1159 position.total_cost().unwrap(),
1160 2.0,
1161 "Total cost calculation is incorrect."
1162 );
1163 }
1164
1165 #[cfg(feature = "clock")]
1166 #[test]
1167 fn test_position_total_cost_short_size() {
1168 let option = setup_option(
1169 Side::Short,
1170 OptionStyle::Call,
1171 Positive::HUNDRED,
1172 pos_or_panic!(105.0),
1173 pos_or_panic!(10.0),
1174 pos_or_panic!(30.0),
1175 );
1176 let position = Position::new(
1177 option,
1178 pos_or_panic!(5.0),
1179 Utc::now(),
1180 Positive::ONE,
1181 Positive::ONE,
1182 None,
1183 None,
1184 );
1185 assert_eq!(
1186 position.total_cost().unwrap(),
1187 20.0,
1188 "Total cost calculation is incorrect."
1189 );
1190 }
1191
1192 #[cfg(feature = "clock")]
1193 #[test]
1194 fn test_position_check_negative_premium() {
1195 let option = setup_option(
1196 Side::Long,
1197 OptionStyle::Call,
1198 Positive::HUNDRED,
1199 pos_or_panic!(110.0),
1200 Positive::ONE,
1201 Positive::ZERO,
1202 );
1203 let position = Position::new(
1204 option,
1205 pos_or_panic!(5.0),
1206 Utc::now(),
1207 Positive::ONE,
1208 Positive::ONE,
1209 None,
1210 None,
1211 );
1212 assert_eq!(
1213 position.pnl_at_expiration(&None).unwrap(),
1214 dec!(3.0),
1215 "PNL at expiration for long call ITM is incorrect."
1216 );
1217 }
1218
1219 #[cfg(feature = "clock")]
1220 #[test]
1221 fn test_position_pnl_at_expiration_long_call_itm() {
1222 let option = setup_option(
1223 Side::Long,
1224 OptionStyle::Call,
1225 Positive::HUNDRED,
1226 pos_or_panic!(110.0),
1227 Positive::ONE,
1228 Positive::ZERO,
1229 );
1230 let position = Position::new(
1231 option,
1232 pos_or_panic!(5.0),
1233 Utc::now(),
1234 Positive::ONE,
1235 Positive::ONE,
1236 None,
1237 None,
1238 );
1239 assert_eq!(
1240 position.pnl_at_expiration(&None).unwrap(),
1241 dec!(3.0),
1242 "PNL at expiration for long call ITM is incorrect."
1243 );
1244 }
1245
1246 #[cfg(feature = "clock")]
1247 #[test]
1248 fn test_position_pnl_at_expiration_long_call_itm_quantity() {
1249 let option = setup_option(
1250 Side::Long,
1251 OptionStyle::Call,
1252 Positive::HUNDRED,
1253 pos_or_panic!(110.0),
1254 pos_or_panic!(10.0),
1255 Positive::ZERO,
1256 );
1257 let position = Position::new(
1258 option,
1259 pos_or_panic!(5.0),
1260 Utc::now(),
1261 Positive::ONE,
1262 Positive::ONE,
1263 None,
1264 None,
1265 );
1266 assert_eq!(
1267 position.pnl_at_expiration(&None).unwrap(),
1268 dec!(30.0),
1269 "PNL at expiration for long call ITM is incorrect."
1270 );
1271 }
1272
1273 #[cfg(feature = "clock")]
1274 #[test]
1275 fn test_position_pnl_at_expiration_short_call_itm() {
1276 let option = setup_option(
1277 Side::Short,
1278 OptionStyle::Call,
1279 Positive::HUNDRED,
1280 pos_or_panic!(110.0),
1281 Positive::ONE,
1282 Positive::ZERO,
1283 );
1284 let position = Position::new(
1285 option,
1286 pos_or_panic!(5.0),
1287 Utc::now(),
1288 Positive::ONE,
1289 Positive::ONE,
1290 None,
1291 None,
1292 );
1293 assert_eq!(
1294 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1295 -7.0,
1296 "PNL at expiration for short call ITM is incorrect."
1297 );
1298 }
1299
1300 #[cfg(feature = "clock")]
1301 #[test]
1302 fn test_position_pnl_at_expiration_short_call_itm_quantity() {
1303 let option = setup_option(
1304 Side::Short,
1305 OptionStyle::Call,
1306 Positive::HUNDRED,
1307 pos_or_panic!(110.0),
1308 pos_or_panic!(10.0),
1309 Positive::ZERO,
1310 );
1311 let position = Position::new(
1312 option,
1313 pos_or_panic!(5.0),
1314 Utc::now(),
1315 Positive::ONE,
1316 Positive::ONE,
1317 None,
1318 None,
1319 );
1320 assert_eq!(
1321 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1322 -70.0,
1323 "PNL at expiration for short call ITM is incorrect."
1324 );
1325 }
1326
1327 #[cfg(feature = "clock")]
1328 #[test]
1329 fn test_position_pnl_at_expiration_long_put_itm() {
1330 let option = setup_option(
1331 Side::Long,
1332 OptionStyle::Put,
1333 Positive::HUNDRED,
1334 pos_or_panic!(90.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 long put ITM is incorrect."
1351 );
1352 }
1353
1354 #[cfg(feature = "clock")]
1355 #[test]
1356 fn test_position_pnl_at_expiration_long_put_itm_quantity() {
1357 let option = setup_option(
1358 Side::Long,
1359 OptionStyle::Put,
1360 Positive::HUNDRED,
1361 pos_or_panic!(90.0),
1362 pos_or_panic!(10.0),
1363 Positive::ZERO,
1364 );
1365 let position = Position::new(
1366 option,
1367 pos_or_panic!(5.0),
1368 Utc::now(),
1369 Positive::ONE,
1370 Positive::ONE,
1371 None,
1372 None,
1373 );
1374 assert_eq!(
1375 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1376 30.0,
1377 "PNL at expiration for long put ITM is incorrect."
1378 );
1379 }
1380
1381 #[cfg(feature = "clock")]
1382 #[test]
1383 fn test_position_pnl_at_expiration_short_put_itm() {
1384 let option = setup_option(
1385 Side::Short,
1386 OptionStyle::Put,
1387 Positive::HUNDRED,
1388 pos_or_panic!(90.0),
1389 Positive::ONE,
1390 Positive::ZERO,
1391 );
1392 let position = Position::new(
1393 option,
1394 pos_or_panic!(5.0),
1395 Utc::now(),
1396 Positive::ONE,
1397 Positive::ONE,
1398 None,
1399 None,
1400 );
1401 assert_eq!(
1402 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1403 -7.0,
1404 "PNL at expiration for short put ITM is incorrect."
1405 );
1406 }
1407
1408 #[cfg(feature = "clock")]
1409 #[test]
1410 fn test_position_pnl_at_expiration_short_put_itm_quantity() {
1411 let option = setup_option(
1412 Side::Short,
1413 OptionStyle::Put,
1414 Positive::HUNDRED,
1415 pos_or_panic!(90.0),
1416 pos_or_panic!(10.0),
1417 Positive::ZERO,
1418 );
1419 let position = Position::new(
1420 option,
1421 pos_or_panic!(5.0),
1422 Utc::now(),
1423 Positive::ONE,
1424 Positive::ONE,
1425 None,
1426 None,
1427 );
1428 assert_eq!(
1429 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1430 -70.0,
1431 "PNL at expiration for short put ITM is incorrect."
1432 );
1433 }
1434
1435 #[cfg(feature = "clock")]
1436 #[test]
1437 fn test_position_pnl_at_expiration_short_put_itm_winning() {
1438 let option = setup_option(
1439 Side::Short,
1440 OptionStyle::Put,
1441 Positive::HUNDRED,
1442 pos_or_panic!(110.0),
1443 Positive::ONE,
1444 Positive::ZERO,
1445 );
1446 let position = Position::new(
1447 option,
1448 pos_or_panic!(5.0),
1449 Utc::now(),
1450 Positive::ONE,
1451 Positive::ONE,
1452 None,
1453 None,
1454 );
1455 assert_eq!(
1456 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1457 3.0,
1458 "PNL at expiration for short put ITM is incorrect."
1459 );
1460 }
1461
1462 #[cfg(feature = "clock")]
1463 #[test]
1464 fn test_position_pnl_at_expiration_short_put_itm_quantity_winning() {
1465 let option = setup_option(
1466 Side::Short,
1467 OptionStyle::Put,
1468 Positive::HUNDRED,
1469 pos_or_panic!(110.0),
1470 pos_or_panic!(10.0),
1471 Positive::ZERO,
1472 );
1473 let position = Position::new(
1474 option,
1475 pos_or_panic!(5.0),
1476 Utc::now(),
1477 Positive::ONE,
1478 Positive::ONE,
1479 None,
1480 None,
1481 );
1482 assert_eq!(
1483 position.pnl_at_expiration(&None).unwrap().to_f64().unwrap(),
1484 30.0,
1485 "PNL at expiration for short put ITM is incorrect."
1486 );
1487 }
1488
1489 #[cfg(feature = "clock")]
1490 #[test]
1491 fn test_unrealized_pnl_long_call() {
1492 let option = setup_option(
1493 Side::Long,
1494 OptionStyle::Call,
1495 Positive::HUNDRED,
1496 pos_or_panic!(105.0),
1497 Positive::ONE,
1498 pos_or_panic!(30.0),
1499 );
1500 let position = Position::new(
1501 option,
1502 pos_or_panic!(5.0),
1503 Utc::now(),
1504 Positive::ONE,
1505 Positive::ONE,
1506 None,
1507 None,
1508 );
1509 assert_eq!(
1510 position
1511 .pnl_at_expiration(&Some(&pos_or_panic!(107.0)))
1512 .unwrap(),
1513 Positive::ZERO,
1514 "Unrealized PNL for long call is incorrect."
1515 );
1516 }
1517
1518 #[cfg(feature = "clock")]
1519 #[test]
1520 fn test_unrealized_pnl_long_call_quantity() {
1521 let option = setup_option(
1522 Side::Long,
1523 OptionStyle::Call,
1524 Positive::HUNDRED,
1525 pos_or_panic!(105.0),
1526 pos_or_panic!(10.0),
1527 pos_or_panic!(30.0),
1528 );
1529 let position = Position::new(
1530 option,
1531 pos_or_panic!(5.0),
1532 Utc::now(),
1533 Positive::ONE,
1534 Positive::ONE,
1535 None,
1536 None,
1537 );
1538 assert_eq!(
1539 position
1540 .pnl_at_expiration(&Some(&pos_or_panic!(107.0)))
1541 .unwrap()
1542 .to_f64()
1543 .unwrap(),
1544 ZERO,
1545 "Unrealized PNL for long call is incorrect."
1546 );
1547 }
1548
1549 #[cfg(feature = "clock")]
1550 #[test]
1551 fn test_unrealized_pnl_short_call() {
1552 let option = setup_option(
1553 Side::Short,
1554 OptionStyle::Call,
1555 Positive::HUNDRED,
1556 pos_or_panic!(105.0),
1557 pos_or_panic!(10.0),
1558 pos_or_panic!(30.0),
1559 );
1560 let position = Position::new(
1561 option,
1562 pos_or_panic!(5.0),
1563 Utc::now(),
1564 Positive::ONE,
1565 Positive::ONE,
1566 None,
1567 None,
1568 );
1569 assert_eq!(
1570 position
1571 .unrealized_pnl(pos_or_panic!(3.0))
1572 .unwrap()
1573 .to_f64()
1574 .unwrap(),
1575 ZERO,
1576 "Unrealized PNL for short call is incorrect."
1577 );
1578 }
1579
1580 #[cfg(feature = "clock")]
1581 #[test]
1582 fn test_unrealized_pnl_short_call_bis() {
1583 let option = setup_option(
1584 Side::Short,
1585 OptionStyle::Call,
1586 Positive::HUNDRED,
1587 pos_or_panic!(105.0),
1588 Positive::ONE,
1589 pos_or_panic!(30.0),
1590 );
1591 let position = Position::new(
1592 option,
1593 pos_or_panic!(5.0),
1594 Utc::now(),
1595 Positive::ONE,
1596 Positive::ONE,
1597 None,
1598 None,
1599 );
1600 assert_eq!(
1601 position
1602 .unrealized_pnl(pos_or_panic!(10.0))
1603 .unwrap()
1604 .to_f64()
1605 .unwrap(),
1606 -7.0,
1607 "Unrealized PNL for short call is incorrect."
1608 );
1609 }
1610
1611 #[cfg(feature = "clock")]
1612 #[test]
1613 fn test_days_held() {
1614 let option = setup_option(
1615 Side::Long,
1616 OptionStyle::Call,
1617 Positive::HUNDRED,
1618 pos_or_panic!(105.0),
1619 pos_or_panic!(10.0),
1620 pos_or_panic!(30.0),
1621 );
1622 let date = Utc::now() - Duration::days(10);
1623 let position = Position::new(
1624 option,
1625 pos_or_panic!(5.0),
1626 date,
1627 Positive::ONE,
1628 Positive::ONE,
1629 None,
1630 None,
1631 );
1632 assert_eq!(
1633 position.days_held().unwrap().to_f64(),
1634 10.0,
1635 "Days held calculation is incorrect."
1636 );
1637 }
1638
1639 #[cfg(feature = "clock")]
1640 #[test]
1641 fn test_days_to_expiration() {
1642 let option = setup_option(
1643 Side::Long,
1644 OptionStyle::Call,
1645 Positive::HUNDRED,
1646 pos_or_panic!(105.0),
1647 pos_or_panic!(10.0),
1648 pos_or_panic!(30.0),
1649 );
1650 let position = Position::new(
1651 option,
1652 pos_or_panic!(5.0),
1653 Utc::now(),
1654 Positive::ONE,
1655 Positive::ONE,
1656 None,
1657 None,
1658 );
1659 assert_eq!(
1660 position.days_to_expiration().unwrap().to_f64(),
1661 30.0,
1662 "Days to expiration calculation is incorrect."
1663 );
1664 }
1665
1666 #[cfg(feature = "clock")]
1667 #[test]
1668 fn test_is_long_position() {
1669 let option = setup_option(
1670 Side::Long,
1671 OptionStyle::Call,
1672 Positive::HUNDRED,
1673 pos_or_panic!(105.0),
1674 pos_or_panic!(10.0),
1675 pos_or_panic!(30.0),
1676 );
1677 let position = Position::new(
1678 option,
1679 pos_or_panic!(5.0),
1680 Utc::now(),
1681 Positive::ONE,
1682 Positive::ONE,
1683 None,
1684 None,
1685 );
1686 assert!(
1687 position.is_long(),
1688 "is_long should return true for long positions."
1689 );
1690 assert!(
1691 !position.is_short(),
1692 "is_short should return false for long positions."
1693 );
1694 }
1695
1696 #[cfg(feature = "clock")]
1697 #[test]
1698 fn test_is_short_position() {
1699 let option = setup_option(
1700 Side::Short,
1701 OptionStyle::Call,
1702 Positive::HUNDRED,
1703 pos_or_panic!(105.0),
1704 pos_or_panic!(10.0),
1705 pos_or_panic!(30.0),
1706 );
1707 let position = Position::new(
1708 option,
1709 pos_or_panic!(5.0),
1710 Utc::now(),
1711 Positive::ONE,
1712 Positive::ONE,
1713 None,
1714 None,
1715 );
1716 assert!(
1717 position.is_short(),
1718 "is_short should return true for short positions."
1719 );
1720 assert!(
1721 !position.is_long(),
1722 "is_long should return false for short positions."
1723 );
1724 }
1725}
1726
1727#[cfg(all(test, feature = "clock"))]
1728mod tests_valid_position {
1729 use super::*;
1730
1731 use crate::model::utils::create_sample_position;
1732 use positive::pos_or_panic;
1733
1734 #[test]
1735 fn test_valid_position() {
1736 let position = create_sample_position(
1737 OptionStyle::Call,
1738 Side::Short,
1739 pos_or_panic!(90.0),
1740 Positive::ONE,
1741 pos_or_panic!(95.0),
1742 pos_or_panic!(0.2),
1743 );
1744 assert!(position.validate());
1745 }
1746
1747 #[test]
1748 fn test_zero_premium() {
1749 let mut position = create_sample_position(
1750 OptionStyle::Call,
1751 Side::Short,
1752 pos_or_panic!(90.0),
1753 Positive::ONE,
1754 pos_or_panic!(95.0),
1755 pos_or_panic!(0.2),
1756 );
1757 position.premium = Positive::ZERO;
1758 assert!(!position.validate());
1759 }
1760
1761 #[test]
1762 fn test_invalid_option() {
1763 let mut position = create_sample_position(
1764 OptionStyle::Call,
1765 Side::Short,
1766 pos_or_panic!(90.0),
1767 Positive::ONE,
1768 pos_or_panic!(95.0),
1769 pos_or_panic!(0.2),
1770 );
1771 position.option.strike_price = Positive::ZERO; assert!(!position.validate());
1773 }
1774
1775 #[test]
1777 fn test_position_validate_negative_risk_free_rate_is_valid() {
1778 let mut position = create_sample_position(
1779 OptionStyle::Call,
1780 Side::Short,
1781 pos_or_panic!(90.0),
1782 Positive::ONE,
1783 pos_or_panic!(95.0),
1784 pos_or_panic!(0.2),
1785 );
1786 position.option.risk_free_rate = rust_decimal_macros::dec!(-0.01);
1787 assert!(position.validate());
1788 }
1789
1790 #[test]
1791 fn test_zero_fees() {
1792 let mut position = create_sample_position(
1793 OptionStyle::Call,
1794 Side::Short,
1795 pos_or_panic!(90.0),
1796 Positive::ONE,
1797 pos_or_panic!(95.0),
1798 pos_or_panic!(0.2),
1799 );
1800 position.open_fee = Positive::ZERO;
1801 position.close_fee = Positive::ZERO;
1802 assert!(position.validate());
1803 }
1804}
1805
1806#[cfg(test)]
1807mod tests_position_break_even {
1808 use super::*;
1809
1810 use crate::model::types::{OptionStyle, OptionType, Side};
1811
1812 use positive::pos_or_panic;
1813 use rust_decimal_macros::dec;
1814
1815 fn setup_option(
1816 side: Side,
1817 option_style: OptionStyle,
1818 strike_price: Positive,
1819 underlying_price: Positive,
1820 quantity: Positive,
1821 expiration_days: Positive,
1822 ) -> Options {
1823 Options {
1824 option_type: OptionType::European,
1825 side,
1826 underlying_symbol: "".to_string(),
1827 strike_price,
1828 expiration_date: ExpirationDate::Days(expiration_days),
1829 implied_volatility: pos_or_panic!(0.2),
1830 quantity,
1831 contract_size: Positive::ONE,
1832 underlying_price,
1833 risk_free_rate: dec!(0.01),
1834 option_style,
1835 dividend_yield: Positive::ZERO,
1836 exotic_params: None,
1837 }
1838 }
1839
1840 #[test]
1841 fn test_unrealized_pnl_long_call() {
1842 let option = setup_option(
1843 Side::Long,
1844 OptionStyle::Call,
1845 Positive::HUNDRED,
1846 pos_or_panic!(105.0),
1847 Positive::ONE,
1848 pos_or_panic!(30.0),
1849 );
1850 let position = Position::new(
1851 option,
1852 pos_or_panic!(5.0),
1853 chrono::DateTime::UNIX_EPOCH,
1854 Positive::ONE,
1855 Positive::ONE,
1856 None,
1857 None,
1858 );
1859 assert_eq!(position.break_even().unwrap(), 107.0);
1860 }
1861
1862 #[test]
1863 fn test_unrealized_pnl_long_call_size() {
1864 let option = setup_option(
1865 Side::Long,
1866 OptionStyle::Call,
1867 Positive::HUNDRED,
1868 pos_or_panic!(105.0),
1869 pos_or_panic!(10.0),
1870 pos_or_panic!(30.0),
1871 );
1872 let position = Position::new(
1873 option,
1874 pos_or_panic!(5.0),
1875 chrono::DateTime::UNIX_EPOCH,
1876 Positive::ONE,
1877 Positive::ONE,
1878 None,
1879 None,
1880 );
1881 assert_eq!(position.break_even().unwrap(), 107.0);
1882 }
1883
1884 #[test]
1885 fn test_unrealized_pnl_short_call() {
1886 let option = setup_option(
1887 Side::Short,
1888 OptionStyle::Call,
1889 Positive::HUNDRED,
1890 pos_or_panic!(105.0),
1891 Positive::ONE,
1892 pos_or_panic!(30.0),
1893 );
1894 let position = Position::new(
1895 option,
1896 pos_or_panic!(5.0),
1897 chrono::DateTime::UNIX_EPOCH,
1898 Positive::ONE,
1899 Positive::ONE,
1900 None,
1901 None,
1902 );
1903 assert_eq!(position.break_even().unwrap(), 103.0);
1904 }
1905
1906 #[test]
1907 fn test_unrealized_pnl_short_call_size() {
1908 let option = setup_option(
1909 Side::Short,
1910 OptionStyle::Call,
1911 Positive::HUNDRED,
1912 pos_or_panic!(105.0),
1913 pos_or_panic!(10.0),
1914 pos_or_panic!(30.0),
1915 );
1916 let position = Position::new(
1917 option,
1918 pos_or_panic!(5.0),
1919 chrono::DateTime::UNIX_EPOCH,
1920 Positive::ONE,
1921 Positive::ONE,
1922 None,
1923 None,
1924 );
1925 assert_eq!(position.break_even().unwrap(), 103.0);
1926 }
1927
1928 #[test]
1929 fn test_unrealized_pnl_long_put() {
1930 let option = setup_option(
1931 Side::Long,
1932 OptionStyle::Put,
1933 Positive::HUNDRED,
1934 pos_or_panic!(105.0),
1935 Positive::ONE,
1936 pos_or_panic!(30.0),
1937 );
1938 let position = Position::new(
1939 option,
1940 pos_or_panic!(5.0),
1941 chrono::DateTime::UNIX_EPOCH,
1942 Positive::ONE,
1943 Positive::ONE,
1944 None,
1945 None,
1946 );
1947 assert_eq!(position.break_even().unwrap(), 93.0);
1948 }
1949
1950 #[test]
1951 fn test_unrealized_pnl_long_put_size() {
1952 let option = setup_option(
1953 Side::Long,
1954 OptionStyle::Put,
1955 Positive::HUNDRED,
1956 pos_or_panic!(105.0),
1957 pos_or_panic!(10.0),
1958 pos_or_panic!(30.0),
1959 );
1960 let position = Position::new(
1961 option,
1962 pos_or_panic!(5.0),
1963 chrono::DateTime::UNIX_EPOCH,
1964 Positive::ONE,
1965 Positive::ONE,
1966 None,
1967 None,
1968 );
1969 assert_eq!(position.break_even().unwrap(), 93.0);
1970 }
1971
1972 #[test]
1973 fn test_unrealized_pnl_short_put() {
1974 let option = setup_option(
1975 Side::Short,
1976 OptionStyle::Put,
1977 Positive::HUNDRED,
1978 pos_or_panic!(105.0),
1979 Positive::ONE,
1980 pos_or_panic!(30.0),
1981 );
1982 let position = Position::new(
1983 option,
1984 pos_or_panic!(5.0),
1985 chrono::DateTime::UNIX_EPOCH,
1986 Positive::ONE,
1987 Positive::ONE,
1988 None,
1989 None,
1990 );
1991 assert_eq!(position.break_even().unwrap(), 97.0);
1992 }
1993
1994 #[test]
1995 fn test_unrealized_pnl_short_put_size() {
1996 let option = setup_option(
1997 Side::Short,
1998 OptionStyle::Put,
1999 Positive::HUNDRED,
2000 pos_or_panic!(105.0),
2001 pos_or_panic!(10.0),
2002 pos_or_panic!(30.0),
2003 );
2004 let position = Position::new(
2005 option,
2006 pos_or_panic!(5.0),
2007 chrono::DateTime::UNIX_EPOCH,
2008 Positive::ONE,
2009 Positive::ONE,
2010 None,
2011 None,
2012 );
2013 assert_eq!(position.break_even().unwrap(), 97.0);
2014 }
2015
2016 #[test]
2017 fn test_zero_quantity() {
2018 let option = setup_option(
2019 Side::Short,
2020 OptionStyle::Put,
2021 Positive::HUNDRED,
2022 pos_or_panic!(105.0),
2023 Positive::ZERO, pos_or_panic!(30.0),
2025 );
2026 let position = Position::new(
2027 option,
2028 pos_or_panic!(5.0),
2029 chrono::DateTime::UNIX_EPOCH,
2030 Positive::ONE,
2031 Positive::ONE,
2032 None,
2033 None,
2034 );
2035 assert!(position.break_even().is_none());
2036 }
2037}
2038
2039#[cfg(test)]
2040mod tests_position_max_loss_profit {
2041 use super::*;
2042
2043 use crate::model::types::{OptionStyle, OptionType, Side};
2044
2045 use approx::assert_relative_eq;
2046 use positive::pos_or_panic;
2047 use rust_decimal_macros::dec;
2048
2049 fn setup_option(
2050 side: Side,
2051 option_style: OptionStyle,
2052 strike_price: Positive,
2053 underlying_price: Positive,
2054 quantity: Positive,
2055 expiration_days: Positive,
2056 ) -> Options {
2057 Options {
2058 option_type: OptionType::European,
2059 side,
2060 underlying_symbol: "".to_string(),
2061 strike_price,
2062 expiration_date: ExpirationDate::Days(expiration_days),
2063 implied_volatility: pos_or_panic!(0.2),
2064 quantity,
2065 contract_size: Positive::ONE,
2066 underlying_price,
2067 risk_free_rate: dec!(0.01),
2068 option_style,
2069 dividend_yield: Positive::ZERO,
2070 exotic_params: None,
2071 }
2072 }
2073
2074 #[test]
2075 fn test_unrealized_pnl_long_call() {
2076 let option = setup_option(
2077 Side::Long,
2078 OptionStyle::Call,
2079 Positive::HUNDRED,
2080 pos_or_panic!(105.0),
2081 Positive::ONE,
2082 pos_or_panic!(30.0),
2083 );
2084 let position = Position::new(
2085 option,
2086 pos_or_panic!(5.0),
2087 chrono::DateTime::UNIX_EPOCH,
2088 Positive::ONE,
2089 Positive::ONE,
2090 None,
2091 None,
2092 );
2093 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 7.0, epsilon = 0.001);
2094 assert_eq!(position.max_profit().unwrap(), Positive::MAX);
2095 }
2096
2097 #[test]
2098 fn test_unrealized_pnl_long_call_size() {
2099 let option = setup_option(
2100 Side::Long,
2101 OptionStyle::Call,
2102 Positive::HUNDRED,
2103 pos_or_panic!(105.0),
2104 pos_or_panic!(10.0),
2105 pos_or_panic!(30.0),
2106 );
2107 let position = Position::new(
2108 option,
2109 pos_or_panic!(5.0),
2110 chrono::DateTime::UNIX_EPOCH,
2111 Positive::ONE,
2112 Positive::ONE,
2113 None,
2114 None,
2115 );
2116 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 70.0, epsilon = 0.001);
2117 assert_eq!(position.max_profit().unwrap(), Positive::MAX);
2118 }
2119
2120 #[test]
2121 fn test_unrealized_pnl_short_call() {
2122 let option = setup_option(
2123 Side::Short,
2124 OptionStyle::Call,
2125 Positive::HUNDRED,
2126 pos_or_panic!(105.0),
2127 Positive::ONE,
2128 pos_or_panic!(30.0),
2129 );
2130 let position = Position::new(
2131 option,
2132 pos_or_panic!(5.0),
2133 chrono::DateTime::UNIX_EPOCH,
2134 Positive::ONE,
2135 Positive::ONE,
2136 None,
2137 None,
2138 );
2139 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2140 assert_relative_eq!(
2141 position.max_profit().unwrap().to_f64(),
2142 3.0,
2143 epsilon = 0.001
2144 );
2145 }
2146
2147 #[test]
2148 fn test_unrealized_pnl_short_call_size() {
2149 let option = setup_option(
2150 Side::Short,
2151 OptionStyle::Call,
2152 Positive::HUNDRED,
2153 pos_or_panic!(105.0),
2154 pos_or_panic!(10.0),
2155 pos_or_panic!(30.0),
2156 );
2157 let position = Position::new(
2158 option,
2159 pos_or_panic!(5.0),
2160 chrono::DateTime::UNIX_EPOCH,
2161 Positive::ONE,
2162 Positive::ONE,
2163 None,
2164 None,
2165 );
2166 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2167 assert_relative_eq!(
2168 position.max_profit().unwrap().to_f64(),
2169 30.0,
2170 epsilon = 0.001
2171 );
2172 }
2173
2174 #[test]
2175 fn test_unrealized_pnl_long_put() {
2176 let option = setup_option(
2177 Side::Long,
2178 OptionStyle::Put,
2179 Positive::HUNDRED,
2180 pos_or_panic!(105.0),
2181 Positive::ONE,
2182 pos_or_panic!(30.0),
2183 );
2184 let position = Position::new(
2185 option,
2186 pos_or_panic!(5.0),
2187 chrono::DateTime::UNIX_EPOCH,
2188 Positive::ONE,
2189 Positive::ONE,
2190 None,
2191 None,
2192 );
2193 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 7.0, epsilon = 0.001);
2194 assert_relative_eq!(position.max_profit().unwrap(), Positive::MAX);
2195 }
2196
2197 #[test]
2198 fn test_unrealized_pnl_long_put_size() {
2199 let option = setup_option(
2200 Side::Long,
2201 OptionStyle::Put,
2202 Positive::HUNDRED,
2203 pos_or_panic!(105.0),
2204 pos_or_panic!(10.0),
2205 pos_or_panic!(30.0),
2206 );
2207 let position = Position::new(
2208 option,
2209 pos_or_panic!(5.0),
2210 chrono::DateTime::UNIX_EPOCH,
2211 Positive::ONE,
2212 Positive::ONE,
2213 None,
2214 None,
2215 );
2216 assert_relative_eq!(position.max_loss().unwrap().to_f64(), 70.0, epsilon = 0.001);
2217 assert_relative_eq!(position.max_profit().unwrap(), Positive::MAX);
2218 }
2219
2220 #[test]
2221 fn test_unrealized_pnl_short_put() {
2222 let option = setup_option(
2223 Side::Short,
2224 OptionStyle::Put,
2225 Positive::HUNDRED,
2226 pos_or_panic!(105.0),
2227 Positive::ONE,
2228 pos_or_panic!(30.0),
2229 );
2230 let position = Position::new(
2231 option,
2232 pos_or_panic!(5.0),
2233 chrono::DateTime::UNIX_EPOCH,
2234 Positive::ONE,
2235 Positive::ONE,
2236 None,
2237 None,
2238 );
2239 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2240 assert_relative_eq!(
2241 position.max_profit().unwrap().to_f64(),
2242 3.0,
2243 epsilon = 0.001
2244 );
2245 }
2246
2247 #[test]
2248 fn test_unrealized_pnl_short_put_size() {
2249 let option = setup_option(
2250 Side::Short,
2251 OptionStyle::Put,
2252 Positive::HUNDRED,
2253 pos_or_panic!(105.0),
2254 pos_or_panic!(10.0),
2255 pos_or_panic!(30.0),
2256 );
2257 let position = Position::new(
2258 option,
2259 pos_or_panic!(5.0),
2260 chrono::DateTime::UNIX_EPOCH,
2261 Positive::ONE,
2262 Positive::ONE,
2263 None,
2264 None,
2265 );
2266 assert_relative_eq!(position.max_loss().unwrap(), Positive::MAX);
2267 assert_relative_eq!(
2268 position.max_profit().unwrap().to_f64(),
2269 30.0,
2270 epsilon = 0.001
2271 );
2272 }
2273}
2274
2275#[cfg(test)]
2276mod tests_premium {
2277 use super::*;
2278 use positive::pos_or_panic;
2279
2280 fn setup_basic_position(side: Side) -> Position {
2281 let option = Options {
2282 side,
2283 quantity: Positive::ONE,
2284 ..Default::default()
2285 };
2286
2287 Position::new(
2288 option,
2289 pos_or_panic!(5.0),
2290 chrono::DateTime::UNIX_EPOCH,
2291 Positive::ONE,
2292 Positive::ONE,
2293 None,
2294 None,
2295 )
2296 }
2297
2298 #[test]
2299 fn test_premium_received_long() {
2300 let position = setup_basic_position(Side::Long);
2301 assert_eq!(position.premium_received().unwrap(), Positive::ZERO);
2302 }
2303
2304 #[test]
2305 fn test_premium_received_short() {
2306 let position = setup_basic_position(Side::Short);
2307 assert_eq!(position.premium_received().unwrap(), 5.0);
2308 }
2309
2310 #[test]
2311 fn test_net_premium_received_long() {
2312 let position = setup_basic_position(Side::Long);
2313 assert_eq!(position.net_premium_received().unwrap(), 0.0);
2314 }
2315
2316 #[test]
2317 fn test_net_premium_received_short() {
2318 let position = setup_basic_position(Side::Short);
2319 assert_eq!(position.net_premium_received().unwrap(), 3.0); }
2321
2322 #[test]
2323 fn test_premium_received_with_quantity() {
2324 let side = Side::Short;
2325 let option = Options {
2326 side,
2327 quantity: pos_or_panic!(10.0),
2328 ..Default::default()
2329 };
2330
2331 let position = Position::new(
2332 option,
2333 pos_or_panic!(5.0),
2334 chrono::DateTime::UNIX_EPOCH,
2335 Positive::ONE,
2336 Positive::ONE,
2337 None,
2338 None,
2339 );
2340 assert_eq!(position.premium_received().unwrap(), 50.0);
2341 }
2342}
2343
2344#[cfg(all(test, feature = "clock"))]
2345mod tests_position_serde {
2346 use super::*;
2347
2348 use crate::model::utils::create_sample_position;
2349
2350 use positive::pos_or_panic;
2351 use serde_json;
2352 use tracing::info;
2353
2354 #[test]
2355 fn test_position_serialization() {
2356 let position = create_sample_position(
2357 OptionStyle::Call,
2358 Side::Short,
2359 pos_or_panic!(90.0),
2360 Positive::ONE,
2361 pos_or_panic!(95.0),
2362 pos_or_panic!(0.2),
2363 );
2364 let serialized = serde_json::to_string(&position).unwrap();
2365
2366 assert!(serialized.contains("\"option\""));
2368 assert!(serialized.contains("\"premium\""));
2369 assert!(serialized.contains("\"date\""));
2370 assert!(serialized.contains("\"open_fee\""));
2371 assert!(serialized.contains("\"close_fee\""));
2372 assert!(serialized.contains("AAPL"));
2373 assert!(serialized.contains("95"));
2374 }
2375
2376 #[test]
2377 fn test_position_deserialization() {
2378 let position = create_sample_position(
2379 OptionStyle::Call,
2380 Side::Short,
2381 pos_or_panic!(90.0),
2382 Positive::ONE,
2383 pos_or_panic!(95.0),
2384 pos_or_panic!(0.2),
2385 );
2386 let serialized = serde_json::to_string(&position).unwrap();
2387 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2388
2389 assert_eq!(position, deserialized);
2390 assert_eq!(deserialized.option.underlying_symbol, "AAPL");
2391 assert_eq!(deserialized.option.strike_price, pos_or_panic!(95.0));
2392 assert_eq!(deserialized.premium, pos_or_panic!(5.0));
2393 assert_eq!(deserialized.open_fee, pos_or_panic!(0.5));
2394 assert_eq!(deserialized.close_fee, pos_or_panic!(0.5));
2395 }
2396
2397 #[test]
2398 fn test_position_json_structure() {
2399 let position = create_sample_position(
2400 OptionStyle::Call,
2401 Side::Short,
2402 pos_or_panic!(90.0),
2403 Positive::ONE,
2404 pos_or_panic!(95.0),
2405 pos_or_panic!(0.2),
2406 );
2407 let serialized = serde_json::to_string_pretty(&position).unwrap();
2408
2409 info!("Serialized Position:\n{}", serialized);
2410
2411 let value: serde_json::Value = serde_json::from_str(&serialized).unwrap();
2412
2413 assert!(value.is_object());
2415 assert!(value.get("option").is_some());
2416 assert!(value.get("premium").is_some());
2417 assert!(value.get("date").is_some());
2418 assert!(value.get("open_fee").is_some());
2419 assert!(value.get("close_fee").is_some());
2420 }
2421
2422 #[test]
2423 fn test_position_deserialize_invalid_json() {
2424 let invalid_json = r#"{
2425 "option": null,
2426 "premium": 5.0,
2427 "date": "2024-01-01T00:00:00Z",
2428 "open_fee": 1.0,
2429 "close_fee": 1.0
2430 }"#;
2431
2432 let result: Result<Position, serde_json::Error> = serde_json::from_str(invalid_json);
2433 assert!(result.is_err());
2434 }
2435
2436 #[test]
2437 fn test_position_roundtrip() {
2438 let original = create_sample_position(
2439 OptionStyle::Call,
2440 Side::Short,
2441 pos_or_panic!(90.0),
2442 Positive::ONE,
2443 pos_or_panic!(95.0),
2444 pos_or_panic!(0.2),
2445 );
2446 let serialized = serde_json::to_string(&original).unwrap();
2447 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2448
2449 assert_eq!(original, deserialized);
2451
2452 let reserialized = serde_json::to_string(&deserialized).unwrap();
2454 let redeserialized: Position = serde_json::from_str(&reserialized).unwrap();
2455 assert_eq!(deserialized, redeserialized);
2456 }
2457
2458 #[test]
2459 fn test_position_with_different_option_types() {
2460 let put_position = create_sample_position(
2462 OptionStyle::Put,
2463 Side::Short,
2464 pos_or_panic!(90.0),
2465 Positive::ONE,
2466 pos_or_panic!(95.0),
2467 pos_or_panic!(0.2),
2468 );
2469
2470 let serialized = serde_json::to_string(&put_position).unwrap();
2471 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2472
2473 assert_eq!(put_position, deserialized);
2474 assert_eq!(deserialized.option.option_style, OptionStyle::Put);
2475
2476 let short_position = create_sample_position(
2478 OptionStyle::Call,
2479 Side::Short,
2480 pos_or_panic!(90.0),
2481 Positive::ONE,
2482 pos_or_panic!(95.0),
2483 pos_or_panic!(0.2),
2484 );
2485
2486 let serialized = serde_json::to_string(&short_position).unwrap();
2487 let deserialized: Position = serde_json::from_str(&serialized).unwrap();
2488
2489 assert_eq!(short_position, deserialized);
2490 assert_eq!(deserialized.option.side, Side::Short);
2491 }
2492}
2493
2494#[cfg(all(test, feature = "clock"))]
2495mod tests_position_tradeable_trait {
2496 use super::*;
2497
2498 use crate::model::utils::create_sample_position;
2499 use positive::pos_or_panic;
2500
2501 #[test]
2502 fn test_tradeable_trade_ref() {
2503 let position = create_sample_position(
2504 OptionStyle::Call,
2505 Side::Short,
2506 pos_or_panic!(90.0),
2507 Positive::ONE,
2508 pos_or_panic!(95.0),
2509 pos_or_panic!(0.2),
2510 );
2511
2512 let result = position.trade_ref();
2513 assert!(result.is_err());
2514 }
2515
2516 #[test]
2517 fn test_tradeable_trade_mut() {
2518 let mut position = create_sample_position(
2519 OptionStyle::Call,
2520 Side::Short,
2521 pos_or_panic!(90.0),
2522 Positive::ONE,
2523 pos_or_panic!(95.0),
2524 pos_or_panic!(0.2),
2525 );
2526
2527 let result = position.trade_mut();
2528 assert!(result.is_err());
2529 }
2530}
2531
2532#[cfg(all(test, feature = "clock"))]
2533mod tests_position_tradestatusable_trait {
2534 use super::*;
2535
2536 use crate::model::utils::create_sample_position;
2537 use positive::pos_or_panic;
2538
2539 #[test]
2540 fn test_tradestatusable_expired() {
2541 let position = create_sample_position(
2542 OptionStyle::Call,
2543 Side::Short,
2544 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2549
2550 let result = position.expired();
2551 assert!(result.is_ok());
2552 }
2553
2554 #[test]
2555 fn test_tradestatusable_exercised() {
2556 let position = create_sample_position(
2557 OptionStyle::Call,
2558 Side::Short,
2559 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2564
2565 let result = position.exercised();
2566 assert!(result.is_ok());
2567 }
2568
2569 #[test]
2570 fn test_tradestatusable_assigned() {
2571 let position = create_sample_position(
2572 OptionStyle::Call,
2573 Side::Short,
2574 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2579
2580 let result = position.assigned();
2581 assert!(result.is_ok());
2582 }
2583
2584 #[test]
2585 fn test_tradestatusable_status_other() {
2586 let position = create_sample_position(
2587 OptionStyle::Call,
2588 Side::Short,
2589 pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.2), );
2594
2595 let result = position.status_other();
2596 assert!(result.is_ok());
2597 }
2598
2599 #[test]
2605 fn test_tradestatusable_close_zeros_tiny_premium() {
2606 use crate::model::utils::create_sample_option_simplest;
2607
2608 let build = |premium: Positive| {
2609 Position::new(
2610 create_sample_option_simplest(OptionStyle::Call, Side::Long),
2611 premium,
2612 chrono::DateTime::UNIX_EPOCH,
2613 pos_or_panic!(0.01),
2614 pos_or_panic!(0.01),
2615 None,
2616 None,
2617 )
2618 };
2619
2620 let position = build(pos_or_panic!(0.02));
2622 let trade = position.close().expect("close() should succeed");
2623 assert_eq!(trade.premium, pos_or_panic!(0.02));
2624
2625 let position = build(pos_or_panic!(0.01));
2627 let trade = position.close().expect("close() should succeed");
2628 assert_eq!(trade.premium, Positive::ZERO);
2629
2630 let position = build(pos_or_panic!(0.005));
2632 let trade = position.close().expect("close() should succeed");
2633 assert_eq!(trade.premium, Positive::ZERO);
2634 }
2635}
2636
2637#[cfg(test)]
2638mod tests_position_contract_size {
2639 use super::*;
2640 use crate::model::leg::{Leg, LegAble};
2641 use crate::model::utils::create_sample_option;
2642 use positive::pos_or_panic;
2643 use rust_decimal_macros::dec;
2644
2645 fn sized_call(side: Side, contract_size: Positive) -> Position {
2648 let option = create_sample_option(
2649 OptionStyle::Call,
2650 side,
2651 Positive::HUNDRED,
2652 Positive::TWO,
2653 Positive::HUNDRED,
2654 pos_or_panic!(0.2),
2655 )
2656 .with_contract_size(contract_size);
2657 Position::new(
2658 option,
2659 pos_or_panic!(5.0),
2660 chrono::DateTime::UNIX_EPOCH,
2661 pos_or_panic!(0.5),
2662 pos_or_panic!(0.5),
2663 None,
2664 None,
2665 )
2666 }
2667
2668 #[test]
2669 fn test_position_contract_size_fees_stay_per_contract() {
2670 let long = sized_call(Side::Long, Positive::HUNDRED);
2671 assert_eq!(long.fees().ok(), Some(Positive::TWO));
2672 let short = sized_call(Side::Short, Positive::HUNDRED);
2673 assert_eq!(short.fees().ok(), Some(Positive::TWO));
2674 }
2675
2676 #[test]
2677 fn test_position_contract_size_long_cost_scales_premium_not_fees() {
2678 let long = sized_call(Side::Long, Positive::HUNDRED);
2680 assert_eq!(long.total_cost().ok(), Some(pos_or_panic!(1002.0)));
2681 assert_eq!(long.premium_received().ok(), Some(Positive::ZERO));
2682 assert_eq!(long.net_cost().ok(), Some(dec!(1002)));
2683 }
2684
2685 #[test]
2686 fn test_position_contract_size_short_premium_scales_fees_do_not() {
2687 let short = sized_call(Side::Short, Positive::HUNDRED);
2689 assert_eq!(short.total_cost().ok(), Some(Positive::TWO));
2690 assert_eq!(short.premium_received().ok(), Some(pos_or_panic!(1000.0)));
2691 assert_eq!(
2692 short.net_premium_received().ok(),
2693 Some(pos_or_panic!(998.0))
2694 );
2695 assert_eq!(short.net_cost().ok(), Some(dec!(-998)));
2696 }
2697
2698 #[test]
2699 fn test_position_contract_size_pnl_at_expiration_scales() {
2700 let long = sized_call(Side::Long, Positive::HUNDRED);
2702 assert_eq!(
2703 long.pnl_at_expiration(&Some(&pos_or_panic!(120.0))).ok(),
2704 Some(dec!(2998))
2705 );
2706 assert_eq!(
2707 long.pnl_at_expiration(&Some(&pos_or_panic!(90.0))).ok(),
2708 Some(dec!(-1002))
2709 );
2710 let short = sized_call(Side::Short, Positive::HUNDRED);
2712 assert_eq!(
2713 short.pnl_at_expiration(&Some(&pos_or_panic!(110.0))).ok(),
2714 Some(dec!(-1002))
2715 );
2716 assert_eq!(
2717 short.pnl_at_expiration(&Some(&pos_or_panic!(90.0))).ok(),
2718 Some(dec!(998))
2719 );
2720 }
2721
2722 #[test]
2723 fn test_position_contract_size_unrealized_pnl_scales_price_not_fees() {
2724 let long = sized_call(Side::Long, Positive::HUNDRED);
2726 assert_eq!(
2727 long.unrealized_pnl(pos_or_panic!(7.0)).ok(),
2728 Some(dec!(398))
2729 );
2730 let short = sized_call(Side::Short, Positive::HUNDRED);
2731 assert_eq!(
2732 short.unrealized_pnl(pos_or_panic!(3.0)).ok(),
2733 Some(dec!(398))
2734 );
2735 }
2736
2737 #[test]
2738 fn test_position_contract_size_break_even_spreads_fees_over_units() {
2739 let long = sized_call(Side::Long, Positive::HUNDRED);
2741 assert_eq!(long.break_even(), Some(pos_or_panic!(105.01)));
2742 let short = sized_call(Side::Short, Positive::HUNDRED);
2743 assert_eq!(short.break_even(), Some(pos_or_panic!(104.99)));
2744 for position in [long, short] {
2746 let Some(break_even) = position.break_even() else {
2747 panic!("break-even exists");
2748 };
2749 assert_eq!(
2750 position.pnl_at_expiration(&Some(&break_even)).ok(),
2751 Some(Decimal::ZERO)
2752 );
2753 }
2754 }
2755
2756 #[test]
2757 fn test_position_contract_size_one_matches_default() {
2758 let default = sized_call(Side::Long, Positive::ONE);
2759 let mut legacy = default.clone();
2760 legacy.option = create_sample_option(
2761 OptionStyle::Call,
2762 Side::Long,
2763 Positive::HUNDRED,
2764 Positive::TWO,
2765 Positive::HUNDRED,
2766 pos_or_panic!(0.2),
2767 );
2768 assert_eq!(default, legacy);
2769 assert_eq!(default.total_cost().ok(), Some(pos_or_panic!(12.0)));
2770 }
2771
2772 #[cfg(feature = "clock")]
2773 #[test]
2774 fn test_position_contract_size_trade_carries_contract_size() {
2775 let long = sized_call(Side::Long, Positive::HUNDRED);
2776 let Ok(trade) = long.trade() else {
2777 panic!("trade builds");
2778 };
2779 assert_eq!(trade.premium, pos_or_panic!(5.0));
2782 assert_eq!(trade.contract_size, Positive::HUNDRED);
2783 assert_eq!(trade.fee, Positive::ONE);
2784 assert_eq!(trade.quantity, Positive::TWO);
2785 assert_eq!(trade.cost().unwrap(), pos_or_panic!(1002.0));
2787 assert_eq!(trade.income().unwrap(), Positive::ZERO);
2788 assert_eq!(trade.net().unwrap(), dec!(-1002));
2789 let Ok(sold) = sized_call(Side::Short, Positive::HUNDRED).trade() else {
2790 panic!("trade builds");
2791 };
2792 let mut sold = sold;
2793 sold.action = Action::Sell;
2794 assert_eq!(sold.cost().unwrap(), pos_or_panic!(1002.0));
2796 }
2797
2798 #[cfg(feature = "clock")]
2799 #[test]
2800 fn test_position_contract_size_trade_matches_unit_lots() {
2801 let Ok(sized) = sized_call(Side::Long, Positive::HUNDRED).trade() else {
2804 panic!("trade builds");
2805 };
2806 let mut lots = sized.clone();
2807 lots.contract_size = Positive::ONE;
2808 lots.quantity = pos_or_panic!(200.0);
2809 lots.fee = pos_or_panic!(0.01);
2810 assert_eq!(sized.cost().unwrap(), lots.cost().unwrap());
2811 assert_eq!(sized.net().unwrap(), lots.net().unwrap());
2812 }
2813
2814 #[cfg(feature = "clock")]
2815 #[test]
2816 fn test_position_contract_size_trade_serde_round_trip() {
2817 let Ok(trade) = sized_call(Side::Long, Positive::HUNDRED).trade() else {
2818 panic!("trade builds");
2819 };
2820 let Ok(json) = serde_json::to_string(&trade) else {
2821 panic!("trade serializes");
2822 };
2823 let Ok(back) = serde_json::from_str::<Trade>(&json) else {
2824 panic!("trade deserializes");
2825 };
2826 assert_eq!(back.contract_size, Positive::HUNDRED);
2827 assert_eq!(back, trade);
2828 }
2829
2830 #[test]
2831 fn test_position_contract_size_leg_notional_covers_all_units() {
2832 let leg = Leg::from(sized_call(Side::Long, Positive::HUNDRED));
2833 assert_eq!(leg.get_quantity(), Positive::TWO);
2834 assert_eq!(
2835 leg.notional_value(Positive::HUNDRED).ok(),
2836 Some(pos_or_panic!(20000.0))
2837 );
2838 assert_eq!(
2839 leg.pnl_at_price(pos_or_panic!(120.0)).ok(),
2840 Some(dec!(2998))
2841 );
2842 }
2843}