1use crate::presentation::instrument::InstrumentType;
2use crate::presentation::market::MarketState;
3use crate::presentation::order::{Direction, OrderType, Status, TimeInForce};
4use crate::presentation::serialization::string_as_float_opt;
5use lightstreamer_rs::subscription::ItemUpdate;
6use pretty_simple_display::{DebugPretty, DisplaySimple};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::ops::Add;
10
11#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
13pub struct AccountInfo {
14 pub accounts: Vec<Account>,
16}
17
18#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
20pub struct Account {
21 #[serde(rename = "accountId")]
23 pub account_id: String,
24 #[serde(rename = "accountName")]
26 pub account_name: String,
27 #[serde(rename = "accountType")]
29 pub account_type: String,
30 pub balance: AccountBalance,
32 pub currency: String,
34 pub status: String,
36 pub preferred: bool,
38}
39
40#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
42pub struct AccountBalance {
43 pub balance: f64,
45 pub deposit: f64,
47 #[serde(rename = "profitLoss")]
49 pub profit_loss: f64,
50 pub available: f64,
52}
53
54#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
56pub struct ActivityMetadata {
57 pub paging: Option<ActivityPaging>,
59}
60
61#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
63pub struct ActivityPaging {
64 pub size: Option<i32>,
66 pub next: Option<String>,
68}
69
70#[repr(u8)]
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, DisplaySimple, Deserialize, Serialize)]
73#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
74pub enum ActivityType {
75 EditStopAndLimit,
77 Position,
79 System,
81 WorkingOrder,
83}
84
85#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
87pub struct Activity {
88 pub date: String,
90 #[serde(rename = "dealId", default)]
92 pub deal_id: Option<String>,
93 #[serde(default)]
95 pub epic: Option<String>,
96 #[serde(default)]
98 pub period: Option<String>,
99 #[serde(rename = "dealReference", default)]
101 pub deal_reference: Option<String>,
102 #[serde(rename = "type")]
104 pub activity_type: ActivityType,
105 #[serde(default)]
107 pub status: Option<Status>,
108 #[serde(default)]
110 pub description: Option<String>,
111 #[serde(default)]
114 pub details: Option<ActivityDetails>,
115 #[serde(default)]
117 pub channel: Option<String>,
118 #[serde(default)]
120 pub currency: Option<String>,
121 #[serde(default)]
123 pub level: Option<String>,
124}
125
126#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
129pub struct ActivityDetails {
130 #[serde(rename = "dealReference", default)]
132 pub deal_reference: Option<String>,
133 #[serde(default)]
135 pub actions: Vec<ActivityAction>,
136 #[serde(rename = "marketName", default)]
138 pub market_name: Option<String>,
139 #[serde(rename = "goodTillDate", default)]
141 pub good_till_date: Option<String>,
142 #[serde(default)]
144 pub currency: Option<String>,
145 #[serde(default)]
147 pub size: Option<f64>,
148 #[serde(default)]
150 pub direction: Option<Direction>,
151 #[serde(default)]
153 pub level: Option<f64>,
154 #[serde(rename = "stopLevel", default)]
156 pub stop_level: Option<f64>,
157 #[serde(rename = "stopDistance", default)]
159 pub stop_distance: Option<f64>,
160 #[serde(rename = "guaranteedStop", default)]
162 pub guaranteed_stop: Option<bool>,
163 #[serde(rename = "trailingStopDistance", default)]
165 pub trailing_stop_distance: Option<f64>,
166 #[serde(rename = "trailingStep", default)]
168 pub trailing_step: Option<f64>,
169 #[serde(rename = "limitLevel", default)]
171 pub limit_level: Option<f64>,
172 #[serde(rename = "limitDistance", default)]
174 pub limit_distance: Option<f64>,
175}
176
177#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
179#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
180pub enum ActionType {
181 LimitOrderDeleted,
183 LimitOrderFilled,
185 LimitOrderOpened,
187 LimitOrderRolled,
189 PositionClosed,
191 PositionDeleted,
193 PositionOpened,
195 PositionPartiallyClosed,
197 PositionRolled,
199 StopLimitAmended,
201 StopOrderAmended,
203 StopOrderDeleted,
205 StopOrderFilled,
207 StopOrderOpened,
209 StopOrderRolled,
211 Unknown,
213 WorkingOrderDeleted,
215}
216
217#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
219#[serde(rename_all = "camelCase")]
220pub struct ActivityAction {
221 pub action_type: ActionType,
223 pub affected_deal_id: Option<String>,
225}
226
227#[derive(DebugPretty, Clone, DisplaySimple, Serialize, Deserialize)]
229pub struct Position {
230 pub position: PositionDetails,
232 pub market: PositionMarket,
234 pub pnl: Option<f64>,
236}
237
238impl Position {
239 #[must_use]
271 pub fn pnl(&self) -> f64 {
272 if let Some(pnl) = self.pnl {
273 pnl
274 } else {
275 match self.position.direction {
276 Direction::Buy => {
277 let value = self.position.size * self.position.level;
278 let current_value = self.position.size * self.market.bid.unwrap_or(value);
279 current_value - value
280 }
281 Direction::Sell => {
282 let value = self.position.size * self.position.level;
283 let current_value = self.position.size * self.market.offer.unwrap_or(value);
284 value - current_value
285 }
286 }
287 }
288 }
289
290 pub fn update_pnl(&mut self) {
317 let pnl = match self.position.direction {
318 Direction::Buy => {
319 let value = self.position.size * self.position.level;
320 let current_value = self.position.size * self.market.bid.unwrap_or(value);
321 current_value - value
322 }
323 Direction::Sell => {
324 let value = self.position.size * self.position.level;
325 let current_value = self.position.size * self.market.offer.unwrap_or(value);
326 value - current_value
327 }
328 };
329 self.pnl = Some(pnl);
330 }
331}
332
333impl Position {
334 #[must_use]
347 #[inline]
348 pub fn is_call(&self) -> bool {
349 self.market.instrument_name.contains("CALL")
350 }
351
352 #[must_use]
364 #[inline]
365 pub fn is_put(&self) -> bool {
366 self.market.instrument_name.contains("PUT")
367 }
368}
369
370impl Add for Position {
371 type Output = Position;
372
373 fn add(self, other: Position) -> Position {
380 debug_assert_eq!(
381 self.market.epic, other.market.epic,
382 "cannot add positions from different markets"
383 );
384 Position {
385 position: self.position + other.position,
386 market: self.market,
387 pnl: match (self.pnl, other.pnl) {
388 (Some(a), Some(b)) => Some(a + b),
389 (Some(a), None) => Some(a),
390 (None, Some(b)) => Some(b),
391 (None, None) => None,
392 },
393 }
394 }
395}
396
397#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
399pub struct PositionDetails {
400 #[serde(rename = "contractSize")]
402 pub contract_size: f64,
403 #[serde(rename = "createdDate")]
405 pub created_date: String,
406 #[serde(rename = "createdDateUTC")]
408 pub created_date_utc: String,
409 #[serde(rename = "dealId")]
411 pub deal_id: String,
412 #[serde(rename = "dealReference")]
414 pub deal_reference: String,
415 pub direction: Direction,
417 #[serde(rename = "limitLevel")]
419 pub limit_level: Option<f64>,
420 pub level: f64,
422 pub size: f64,
424 #[serde(rename = "stopLevel")]
426 pub stop_level: Option<f64>,
427 #[serde(rename = "trailingStep")]
429 pub trailing_step: Option<f64>,
430 #[serde(rename = "trailingStopDistance")]
432 pub trailing_stop_distance: Option<f64>,
433 pub currency: String,
435 #[serde(rename = "controlledRisk")]
437 pub controlled_risk: bool,
438 #[serde(rename = "limitedRiskPremium")]
440 pub limited_risk_premium: Option<f64>,
441}
442
443impl Add for PositionDetails {
444 type Output = PositionDetails;
445
446 fn add(self, other: PositionDetails) -> PositionDetails {
447 let (contract_size, size) = if self.direction != other.direction {
448 (
449 (self.contract_size - other.contract_size).abs(),
450 (self.size - other.size).abs(),
451 )
452 } else {
453 (
454 self.contract_size + other.contract_size,
455 self.size + other.size,
456 )
457 };
458
459 PositionDetails {
460 contract_size,
461 created_date: self.created_date,
462 created_date_utc: self.created_date_utc,
463 deal_id: self.deal_id,
464 deal_reference: self.deal_reference,
465 direction: self.direction,
466 limit_level: other.limit_level.or(self.limit_level),
467 level: (self.level + other.level) / 2.0, size,
469 stop_level: other.stop_level.or(self.stop_level),
470 trailing_step: other.trailing_step.or(self.trailing_step),
471 trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
472 currency: self.currency.clone(),
473 controlled_risk: self.controlled_risk || other.controlled_risk,
474 limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
475 }
476 }
477}
478
479#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
481pub struct PositionMarket {
482 #[serde(rename = "instrumentName")]
484 pub instrument_name: String,
485 pub expiry: String,
487 pub epic: String,
489 #[serde(rename = "instrumentType")]
491 pub instrument_type: String,
492 #[serde(rename = "lotSize")]
494 pub lot_size: f64,
495 pub high: Option<f64>,
497 pub low: Option<f64>,
499 #[serde(rename = "percentageChange")]
501 pub percentage_change: f64,
502 #[serde(rename = "netChange")]
504 pub net_change: f64,
505 pub bid: Option<f64>,
507 pub offer: Option<f64>,
509 #[serde(rename = "updateTime")]
511 pub update_time: String,
512 #[serde(rename = "updateTimeUTC")]
514 pub update_time_utc: String,
515 #[serde(rename = "delayTime")]
517 pub delay_time: i64,
518 #[serde(rename = "streamingPricesAvailable")]
520 pub streaming_prices_available: bool,
521 #[serde(rename = "marketStatus")]
523 pub market_status: String,
524 #[serde(rename = "scalingFactor")]
526 pub scaling_factor: i64,
527}
528
529impl PositionMarket {
530 pub fn is_call(&self) -> bool {
543 self.instrument_name.contains("CALL")
544 }
545
546 pub fn is_put(&self) -> bool {
558 self.instrument_name.contains("PUT")
559 }
560}
561
562#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
564pub struct WorkingOrder {
565 #[serde(rename = "workingOrderData")]
567 pub working_order_data: WorkingOrderData,
568 #[serde(rename = "marketData")]
570 pub market_data: AccountMarketData,
571}
572
573#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
575pub struct WorkingOrderData {
576 #[serde(rename = "dealId")]
578 pub deal_id: String,
579 pub direction: Direction,
581 pub epic: String,
583 #[serde(rename = "orderSize")]
585 pub order_size: f64,
586 #[serde(rename = "orderLevel")]
588 pub order_level: f64,
589 #[serde(rename = "timeInForce")]
591 pub time_in_force: TimeInForce,
592 #[serde(rename = "goodTillDate")]
594 pub good_till_date: Option<String>,
595 #[serde(rename = "goodTillDateISO")]
597 pub good_till_date_iso: Option<String>,
598 #[serde(rename = "createdDate")]
600 pub created_date: String,
601 #[serde(rename = "createdDateUTC")]
603 pub created_date_utc: String,
604 #[serde(rename = "guaranteedStop")]
606 pub guaranteed_stop: bool,
607 #[serde(rename = "orderType")]
609 pub order_type: OrderType,
610 #[serde(rename = "stopDistance")]
612 pub stop_distance: Option<f64>,
613 #[serde(rename = "limitDistance")]
615 pub limit_distance: Option<f64>,
616 #[serde(rename = "currencyCode")]
618 pub currency_code: String,
619 pub dma: bool,
621 #[serde(rename = "limitedRiskPremium")]
623 pub limited_risk_premium: Option<f64>,
624 #[serde(rename = "limitLevel", default)]
626 pub limit_level: Option<f64>,
627 #[serde(rename = "stopLevel", default)]
629 pub stop_level: Option<f64>,
630 #[serde(rename = "dealReference", default)]
632 pub deal_reference: Option<String>,
633}
634
635#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
637pub struct AccountMarketData {
638 #[serde(rename = "instrumentName")]
640 pub instrument_name: String,
641 #[serde(rename = "exchangeId")]
643 pub exchange_id: String,
644 pub expiry: String,
646 #[serde(rename = "marketStatus")]
648 pub market_status: MarketState,
649 pub epic: String,
651 #[serde(rename = "instrumentType")]
653 pub instrument_type: InstrumentType,
654 #[serde(rename = "lotSize")]
656 pub lot_size: f64,
657 pub high: Option<f64>,
659 pub low: Option<f64>,
661 #[serde(rename = "percentageChange")]
663 pub percentage_change: f64,
664 #[serde(rename = "netChange")]
666 pub net_change: f64,
667 pub bid: Option<f64>,
669 pub offer: Option<f64>,
671 #[serde(rename = "updateTime")]
673 pub update_time: String,
674 #[serde(rename = "updateTimeUTC")]
676 pub update_time_utc: String,
677 #[serde(rename = "delayTime")]
679 pub delay_time: i64,
680 #[serde(rename = "streamingPricesAvailable")]
682 pub streaming_prices_available: bool,
683 #[serde(rename = "scalingFactor")]
685 pub scaling_factor: i64,
686}
687
688impl AccountMarketData {
689 pub fn is_call(&self) -> bool {
702 self.instrument_name.contains("CALL")
703 }
704
705 pub fn is_put(&self) -> bool {
717 self.instrument_name.contains("PUT")
718 }
719}
720
721#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
723pub struct TransactionMetadata {
724 #[serde(rename = "pageData")]
726 pub page_data: PageData,
727 pub size: i32,
729}
730
731#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
733pub struct PageData {
734 #[serde(rename = "pageNumber")]
736 pub page_number: i32,
737 #[serde(rename = "pageSize")]
739 pub page_size: i32,
740 #[serde(rename = "totalPages")]
742 pub total_pages: i32,
743}
744
745#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
747pub struct AccountTransaction {
748 pub date: String,
750 #[serde(rename = "dateUtc")]
752 pub date_utc: String,
753 #[serde(rename = "openDateUtc")]
755 pub open_date_utc: String,
756 #[serde(rename = "instrumentName")]
758 pub instrument_name: String,
759 pub period: String,
761 #[serde(rename = "profitAndLoss")]
763 pub profit_and_loss: String,
764 #[serde(rename = "transactionType")]
766 pub transaction_type: String,
767 pub reference: String,
769 #[serde(rename = "openLevel")]
771 pub open_level: String,
772 #[serde(rename = "closeLevel")]
774 pub close_level: String,
775 pub size: String,
777 pub currency: String,
779 #[serde(rename = "cashTransaction")]
781 pub cash_transaction: bool,
782}
783
784impl AccountTransaction {
785 pub fn is_call(&self) -> bool {
798 self.instrument_name.contains("CALL")
799 }
800
801 pub fn is_put(&self) -> bool {
813 self.instrument_name.contains("PUT")
814 }
815}
816
817#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
819pub struct AccountData {
820 pub item_name: String,
822 pub item_pos: i32,
824 pub fields: AccountFields,
826 pub changed_fields: AccountFields,
828 pub is_snapshot: bool,
830}
831
832#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
834pub struct AccountFields {
835 #[serde(rename = "PNL")]
836 #[serde(with = "string_as_float_opt")]
837 #[serde(skip_serializing_if = "Option::is_none")]
838 pnl: Option<f64>,
839
840 #[serde(rename = "DEPOSIT")]
841 #[serde(with = "string_as_float_opt")]
842 #[serde(skip_serializing_if = "Option::is_none")]
843 deposit: Option<f64>,
844
845 #[serde(rename = "AVAILABLE_CASH")]
846 #[serde(with = "string_as_float_opt")]
847 #[serde(skip_serializing_if = "Option::is_none")]
848 available_cash: Option<f64>,
849
850 #[serde(rename = "PNL_LR")]
851 #[serde(with = "string_as_float_opt")]
852 #[serde(skip_serializing_if = "Option::is_none")]
853 pnl_lr: Option<f64>,
854
855 #[serde(rename = "PNL_NLR")]
856 #[serde(with = "string_as_float_opt")]
857 #[serde(skip_serializing_if = "Option::is_none")]
858 pnl_nlr: Option<f64>,
859
860 #[serde(rename = "FUNDS")]
861 #[serde(with = "string_as_float_opt")]
862 #[serde(skip_serializing_if = "Option::is_none")]
863 funds: Option<f64>,
864
865 #[serde(rename = "MARGIN")]
866 #[serde(with = "string_as_float_opt")]
867 #[serde(skip_serializing_if = "Option::is_none")]
868 margin: Option<f64>,
869
870 #[serde(rename = "MARGIN_LR")]
871 #[serde(with = "string_as_float_opt")]
872 #[serde(skip_serializing_if = "Option::is_none")]
873 margin_lr: Option<f64>,
874
875 #[serde(rename = "MARGIN_NLR")]
876 #[serde(with = "string_as_float_opt")]
877 #[serde(skip_serializing_if = "Option::is_none")]
878 margin_nlr: Option<f64>,
879
880 #[serde(rename = "AVAILABLE_TO_DEAL")]
881 #[serde(with = "string_as_float_opt")]
882 #[serde(skip_serializing_if = "Option::is_none")]
883 available_to_deal: Option<f64>,
884
885 #[serde(rename = "EQUITY")]
886 #[serde(with = "string_as_float_opt")]
887 #[serde(skip_serializing_if = "Option::is_none")]
888 equity: Option<f64>,
889
890 #[serde(rename = "EQUITY_USED")]
891 #[serde(with = "string_as_float_opt")]
892 #[serde(skip_serializing_if = "Option::is_none")]
893 equity_used: Option<f64>,
894}
895
896impl AccountData {
897 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
905 let item_name = item_update.item_name.clone().unwrap_or_default();
907
908 let item_pos = item_update.item_pos as i32;
910
911 let is_snapshot = item_update.is_snapshot;
913
914 let fields = Self::create_account_fields(&item_update.fields)?;
916
917 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
919 for (key, value) in &item_update.changed_fields {
920 changed_fields_map.insert(key.clone(), Some(value.clone()));
921 }
922 let changed_fields = Self::create_account_fields(&changed_fields_map)?;
923
924 Ok(AccountData {
925 item_name,
926 item_pos,
927 fields,
928 changed_fields,
929 is_snapshot,
930 })
931 }
932
933 fn create_account_fields(
941 fields_map: &HashMap<String, Option<String>>,
942 ) -> Result<AccountFields, String> {
943 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
945
946 let parse_float = |key: &str| -> Result<Option<f64>, String> {
948 match get_field(key) {
949 Some(val) if !val.is_empty() => val
950 .parse::<f64>()
951 .map(Some)
952 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
953 _ => Ok(None),
954 }
955 };
956
957 Ok(AccountFields {
958 pnl: parse_float("PNL")?,
959 deposit: parse_float("DEPOSIT")?,
960 available_cash: parse_float("AVAILABLE_CASH")?,
961 pnl_lr: parse_float("PNL_LR")?,
962 pnl_nlr: parse_float("PNL_NLR")?,
963 funds: parse_float("FUNDS")?,
964 margin: parse_float("MARGIN")?,
965 margin_lr: parse_float("MARGIN_LR")?,
966 margin_nlr: parse_float("MARGIN_NLR")?,
967 available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
968 equity: parse_float("EQUITY")?,
969 equity_used: parse_float("EQUITY_USED")?,
970 })
971 }
972}
973
974impl From<&ItemUpdate> for AccountData {
975 fn from(item_update: &ItemUpdate) -> Self {
976 Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use crate::presentation::order::Direction;
984
985 fn sample_position_details(direction: Direction, level: f64, size: f64) -> PositionDetails {
986 PositionDetails {
987 contract_size: 1.0,
988 created_date: "2025/10/30 18:13:53:000".to_string(),
989 created_date_utc: "2025-10-30T17:13:53".to_string(),
990 deal_id: "DIAAAAVJNQPWZAG".to_string(),
991 deal_reference: "RZ0RQ1K8V1S1JN2".to_string(),
992 direction,
993 limit_level: None,
994 level,
995 size,
996 stop_level: None,
997 trailing_step: None,
998 trailing_stop_distance: None,
999 currency: "USD".to_string(),
1000 controlled_risk: false,
1001 limited_risk_premium: None,
1002 }
1003 }
1004
1005 fn sample_market(bid: Option<f64>, offer: Option<f64>) -> PositionMarket {
1006 PositionMarket {
1007 instrument_name: "US 500 6910 PUT ($1)".to_string(),
1008 expiry: "DEC-25".to_string(),
1009 epic: "OP.D.OTCSPX3.6910P.IP".to_string(),
1010 instrument_type: "UNKNOWN".to_string(),
1011 lot_size: 1.0,
1012 high: Some(153.43),
1013 low: Some(147.42),
1014 percentage_change: 0.61,
1015 net_change: 6895.38,
1016 bid,
1017 offer,
1018 update_time: "05:55:59".to_string(),
1019 update_time_utc: "05:55:59".to_string(),
1020 delay_time: 0,
1021 streaming_prices_available: true,
1022 market_status: "TRADEABLE".to_string(),
1023 scaling_factor: 1,
1024 }
1025 }
1026
1027 #[test]
1028 fn pnl_sell_uses_offer_and_matches_sample_data() {
1029 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1033 let market = sample_market(Some(151.32), Some(152.82));
1034 let position = Position {
1035 position: details,
1036 market,
1037 pnl: None,
1038 };
1039
1040 let pnl = position.pnl();
1041 assert!((pnl - 2.32).abs() < 1e-9, "expected 2.32, got {}", pnl);
1042 }
1043
1044 #[test]
1045 fn pnl_buy_uses_bid_and_computes_difference() {
1046 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1049 let market = sample_market(Some(151.32), Some(152.82));
1050 let position = Position {
1051 position: details,
1052 market,
1053 pnl: None,
1054 };
1055
1056 let pnl = position.pnl();
1057 assert!((pnl + 3.82).abs() < 1e-9, "expected -3.82, got {}", pnl);
1058 }
1059
1060 #[test]
1061 fn pnl_field_overrides_calculation_when_present() {
1062 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1063 let market = sample_market(Some(151.32), Some(152.82));
1064 let position = Position {
1066 position: details,
1067 market,
1068 pnl: Some(10.0),
1069 };
1070 assert_eq!(position.pnl(), 10.0);
1071 }
1072
1073 #[test]
1074 fn pnl_sell_is_zero_when_offer_missing() {
1075 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1077 let market = sample_market(Some(151.32), None);
1078 let position = Position {
1079 position: details,
1080 market,
1081 pnl: None,
1082 };
1083 assert!((position.pnl() - 0.0).abs() < 1e-12);
1084 }
1085
1086 #[test]
1087 fn pnl_buy_is_zero_when_bid_missing() {
1088 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1090 let market = sample_market(None, Some(152.82));
1091 let position = Position {
1092 position: details,
1093 market,
1094 pnl: None,
1095 };
1096 assert!((position.pnl() - 0.0).abs() < 1e-12);
1097 }
1098}