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#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
71#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
73pub enum ActivityType {
74 EditStopAndLimit,
76 Position,
78 System,
80 WorkingOrder,
82}
83
84#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
86pub struct Activity {
87 pub date: String,
89 #[serde(rename = "dealId", default)]
91 pub deal_id: Option<String>,
92 #[serde(default)]
94 pub epic: Option<String>,
95 #[serde(default)]
97 pub period: Option<String>,
98 #[serde(rename = "dealReference", default)]
100 pub deal_reference: Option<String>,
101 #[serde(rename = "type")]
103 pub activity_type: ActivityType,
104 #[serde(default)]
106 pub status: Option<Status>,
107 #[serde(default)]
109 pub description: Option<String>,
110 #[serde(default)]
113 pub details: Option<ActivityDetails>,
114 #[serde(default)]
116 pub channel: Option<String>,
117 #[serde(default)]
119 pub currency: Option<String>,
120 #[serde(default)]
122 pub level: Option<String>,
123}
124
125#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
128pub struct ActivityDetails {
129 #[serde(rename = "dealReference", default)]
131 pub deal_reference: Option<String>,
132 #[serde(default)]
134 pub actions: Vec<ActivityAction>,
135 #[serde(rename = "marketName", default)]
137 pub market_name: Option<String>,
138 #[serde(rename = "goodTillDate", default)]
140 pub good_till_date: Option<String>,
141 #[serde(default)]
143 pub currency: Option<String>,
144 #[serde(default)]
146 pub size: Option<f64>,
147 #[serde(default)]
149 pub direction: Option<Direction>,
150 #[serde(default)]
152 pub level: Option<f64>,
153 #[serde(rename = "stopLevel", default)]
155 pub stop_level: Option<f64>,
156 #[serde(rename = "stopDistance", default)]
158 pub stop_distance: Option<f64>,
159 #[serde(rename = "guaranteedStop", default)]
161 pub guaranteed_stop: Option<bool>,
162 #[serde(rename = "trailingStopDistance", default)]
164 pub trailing_stop_distance: Option<f64>,
165 #[serde(rename = "trailingStep", default)]
167 pub trailing_step: Option<f64>,
168 #[serde(rename = "limitLevel", default)]
170 pub limit_level: Option<f64>,
171 #[serde(rename = "limitDistance", default)]
173 pub limit_distance: Option<f64>,
174}
175
176#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
178#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
179pub enum ActionType {
180 LimitOrderDeleted,
182 LimitOrderFilled,
184 LimitOrderOpened,
186 LimitOrderRolled,
188 PositionClosed,
190 PositionDeleted,
192 PositionOpened,
194 PositionPartiallyClosed,
196 PositionRolled,
198 StopLimitAmended,
200 StopOrderAmended,
202 StopOrderDeleted,
204 StopOrderFilled,
206 StopOrderOpened,
208 StopOrderRolled,
210 Unknown,
212 WorkingOrderDeleted,
214}
215
216#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
218#[serde(rename_all = "camelCase")]
219pub struct ActivityAction {
220 pub action_type: ActionType,
222 pub affected_deal_id: Option<String>,
224}
225
226#[derive(DebugPretty, Clone, DisplaySimple, Serialize, Deserialize)]
228pub struct Position {
229 pub position: PositionDetails,
231 pub market: PositionMarket,
233 pub pnl: Option<f64>,
235}
236
237impl Position {
238 pub fn pnl(&self) -> f64 {
270 if let Some(pnl) = self.pnl {
271 pnl
272 } else {
273 match self.position.direction {
274 Direction::Buy => {
275 let value = self.position.size * self.position.level;
276 let current_value = self.position.size * self.market.bid.unwrap_or(value);
277 current_value - value
278 }
279 Direction::Sell => {
280 let value = self.position.size * self.position.level;
281 let current_value = self.position.size * self.market.offer.unwrap_or(value);
282 value - current_value
283 }
284 }
285 }
286 }
287
288 pub fn update_pnl(&mut self) {
315 let pnl = match self.position.direction {
316 Direction::Buy => {
317 let value = self.position.size * self.position.level;
318 let current_value = self.position.size * self.market.bid.unwrap_or(value);
319 current_value - value
320 }
321 Direction::Sell => {
322 let value = self.position.size * self.position.level;
323 let current_value = self.position.size * self.market.offer.unwrap_or(value);
324 value - current_value
325 }
326 };
327 self.pnl = Some(pnl);
328 }
329}
330
331impl Position {
332 pub fn is_call(&self) -> bool {
345 self.market.instrument_name.contains("CALL")
346 }
347
348 pub fn is_put(&self) -> bool {
360 self.market.instrument_name.contains("PUT")
361 }
362}
363
364impl Add for Position {
365 type Output = Position;
366
367 fn add(self, other: Position) -> Position {
368 if self.market.epic != other.market.epic {
369 panic!("Cannot add positions from different markets");
370 }
371 Position {
372 position: self.position + other.position,
373 market: self.market,
374 pnl: match (self.pnl, other.pnl) {
375 (Some(a), Some(b)) => Some(a + b),
376 (Some(a), None) => Some(a),
377 (None, Some(b)) => Some(b),
378 (None, None) => None,
379 },
380 }
381 }
382}
383
384#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
386pub struct PositionDetails {
387 #[serde(rename = "contractSize")]
389 pub contract_size: f64,
390 #[serde(rename = "createdDate")]
392 pub created_date: String,
393 #[serde(rename = "createdDateUTC")]
395 pub created_date_utc: String,
396 #[serde(rename = "dealId")]
398 pub deal_id: String,
399 #[serde(rename = "dealReference")]
401 pub deal_reference: String,
402 pub direction: Direction,
404 #[serde(rename = "limitLevel")]
406 pub limit_level: Option<f64>,
407 pub level: f64,
409 pub size: f64,
411 #[serde(rename = "stopLevel")]
413 pub stop_level: Option<f64>,
414 #[serde(rename = "trailingStep")]
416 pub trailing_step: Option<f64>,
417 #[serde(rename = "trailingStopDistance")]
419 pub trailing_stop_distance: Option<f64>,
420 pub currency: String,
422 #[serde(rename = "controlledRisk")]
424 pub controlled_risk: bool,
425 #[serde(rename = "limitedRiskPremium")]
427 pub limited_risk_premium: Option<f64>,
428}
429
430impl Add for PositionDetails {
431 type Output = PositionDetails;
432
433 fn add(self, other: PositionDetails) -> PositionDetails {
434 let (contract_size, size) = if self.direction != other.direction {
435 (
436 (self.contract_size - other.contract_size).abs(),
437 (self.size - other.size).abs(),
438 )
439 } else {
440 (
441 self.contract_size + other.contract_size,
442 self.size + other.size,
443 )
444 };
445
446 PositionDetails {
447 contract_size,
448 created_date: self.created_date,
449 created_date_utc: self.created_date_utc,
450 deal_id: self.deal_id,
451 deal_reference: self.deal_reference,
452 direction: self.direction,
453 limit_level: other.limit_level.or(self.limit_level),
454 level: (self.level + other.level) / 2.0, size,
456 stop_level: other.stop_level.or(self.stop_level),
457 trailing_step: other.trailing_step.or(self.trailing_step),
458 trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
459 currency: self.currency.clone(),
460 controlled_risk: self.controlled_risk || other.controlled_risk,
461 limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
462 }
463 }
464}
465
466#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
468pub struct PositionMarket {
469 #[serde(rename = "instrumentName")]
471 pub instrument_name: String,
472 pub expiry: String,
474 pub epic: String,
476 #[serde(rename = "instrumentType")]
478 pub instrument_type: String,
479 #[serde(rename = "lotSize")]
481 pub lot_size: f64,
482 pub high: Option<f64>,
484 pub low: Option<f64>,
486 #[serde(rename = "percentageChange")]
488 pub percentage_change: f64,
489 #[serde(rename = "netChange")]
491 pub net_change: f64,
492 pub bid: Option<f64>,
494 pub offer: Option<f64>,
496 #[serde(rename = "updateTime")]
498 pub update_time: String,
499 #[serde(rename = "updateTimeUTC")]
501 pub update_time_utc: String,
502 #[serde(rename = "delayTime")]
504 pub delay_time: i64,
505 #[serde(rename = "streamingPricesAvailable")]
507 pub streaming_prices_available: bool,
508 #[serde(rename = "marketStatus")]
510 pub market_status: String,
511 #[serde(rename = "scalingFactor")]
513 pub scaling_factor: i64,
514}
515
516impl PositionMarket {
517 pub fn is_call(&self) -> bool {
530 self.instrument_name.contains("CALL")
531 }
532
533 pub fn is_put(&self) -> bool {
545 self.instrument_name.contains("PUT")
546 }
547}
548
549#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
551pub struct WorkingOrder {
552 #[serde(rename = "workingOrderData")]
554 pub working_order_data: WorkingOrderData,
555 #[serde(rename = "marketData")]
557 pub market_data: AccountMarketData,
558}
559
560#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
562pub struct WorkingOrderData {
563 #[serde(rename = "dealId")]
565 pub deal_id: String,
566 pub direction: Direction,
568 pub epic: String,
570 #[serde(rename = "orderSize")]
572 pub order_size: f64,
573 #[serde(rename = "orderLevel")]
575 pub order_level: f64,
576 #[serde(rename = "timeInForce")]
578 pub time_in_force: TimeInForce,
579 #[serde(rename = "goodTillDate")]
581 pub good_till_date: Option<String>,
582 #[serde(rename = "goodTillDateISO")]
584 pub good_till_date_iso: Option<String>,
585 #[serde(rename = "createdDate")]
587 pub created_date: String,
588 #[serde(rename = "createdDateUTC")]
590 pub created_date_utc: String,
591 #[serde(rename = "guaranteedStop")]
593 pub guaranteed_stop: bool,
594 #[serde(rename = "orderType")]
596 pub order_type: OrderType,
597 #[serde(rename = "stopDistance")]
599 pub stop_distance: Option<f64>,
600 #[serde(rename = "limitDistance")]
602 pub limit_distance: Option<f64>,
603 #[serde(rename = "currencyCode")]
605 pub currency_code: String,
606 pub dma: bool,
608 #[serde(rename = "limitedRiskPremium")]
610 pub limited_risk_premium: Option<f64>,
611 #[serde(rename = "limitLevel", default)]
613 pub limit_level: Option<f64>,
614 #[serde(rename = "stopLevel", default)]
616 pub stop_level: Option<f64>,
617 #[serde(rename = "dealReference", default)]
619 pub deal_reference: Option<String>,
620}
621
622#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
624pub struct AccountMarketData {
625 #[serde(rename = "instrumentName")]
627 pub instrument_name: String,
628 #[serde(rename = "exchangeId")]
630 pub exchange_id: String,
631 pub expiry: String,
633 #[serde(rename = "marketStatus")]
635 pub market_status: MarketState,
636 pub epic: String,
638 #[serde(rename = "instrumentType")]
640 pub instrument_type: InstrumentType,
641 #[serde(rename = "lotSize")]
643 pub lot_size: f64,
644 pub high: Option<f64>,
646 pub low: Option<f64>,
648 #[serde(rename = "percentageChange")]
650 pub percentage_change: f64,
651 #[serde(rename = "netChange")]
653 pub net_change: f64,
654 pub bid: Option<f64>,
656 pub offer: Option<f64>,
658 #[serde(rename = "updateTime")]
660 pub update_time: String,
661 #[serde(rename = "updateTimeUTC")]
663 pub update_time_utc: String,
664 #[serde(rename = "delayTime")]
666 pub delay_time: i64,
667 #[serde(rename = "streamingPricesAvailable")]
669 pub streaming_prices_available: bool,
670 #[serde(rename = "scalingFactor")]
672 pub scaling_factor: i64,
673}
674
675impl AccountMarketData {
676 pub fn is_call(&self) -> bool {
689 self.instrument_name.contains("CALL")
690 }
691
692 pub fn is_put(&self) -> bool {
704 self.instrument_name.contains("PUT")
705 }
706}
707
708#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
710pub struct TransactionMetadata {
711 #[serde(rename = "pageData")]
713 pub page_data: PageData,
714 pub size: i32,
716}
717
718#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
720pub struct PageData {
721 #[serde(rename = "pageNumber")]
723 pub page_number: i32,
724 #[serde(rename = "pageSize")]
726 pub page_size: i32,
727 #[serde(rename = "totalPages")]
729 pub total_pages: i32,
730}
731
732#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
734pub struct AccountTransaction {
735 pub date: String,
737 #[serde(rename = "dateUtc")]
739 pub date_utc: String,
740 #[serde(rename = "openDateUtc")]
742 pub open_date_utc: String,
743 #[serde(rename = "instrumentName")]
745 pub instrument_name: String,
746 pub period: String,
748 #[serde(rename = "profitAndLoss")]
750 pub profit_and_loss: String,
751 #[serde(rename = "transactionType")]
753 pub transaction_type: String,
754 pub reference: String,
756 #[serde(rename = "openLevel")]
758 pub open_level: String,
759 #[serde(rename = "closeLevel")]
761 pub close_level: String,
762 pub size: String,
764 pub currency: String,
766 #[serde(rename = "cashTransaction")]
768 pub cash_transaction: bool,
769}
770
771impl AccountTransaction {
772 pub fn is_call(&self) -> bool {
785 self.instrument_name.contains("CALL")
786 }
787
788 pub fn is_put(&self) -> bool {
800 self.instrument_name.contains("PUT")
801 }
802}
803
804#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
806pub struct AccountData {
807 pub item_name: String,
809 pub item_pos: i32,
811 pub fields: AccountFields,
813 pub changed_fields: AccountFields,
815 pub is_snapshot: bool,
817}
818
819#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
821pub struct AccountFields {
822 #[serde(rename = "PNL")]
823 #[serde(with = "string_as_float_opt")]
824 #[serde(skip_serializing_if = "Option::is_none")]
825 pnl: Option<f64>,
826
827 #[serde(rename = "DEPOSIT")]
828 #[serde(with = "string_as_float_opt")]
829 #[serde(skip_serializing_if = "Option::is_none")]
830 deposit: Option<f64>,
831
832 #[serde(rename = "AVAILABLE_CASH")]
833 #[serde(with = "string_as_float_opt")]
834 #[serde(skip_serializing_if = "Option::is_none")]
835 available_cash: Option<f64>,
836
837 #[serde(rename = "PNL_LR")]
838 #[serde(with = "string_as_float_opt")]
839 #[serde(skip_serializing_if = "Option::is_none")]
840 pnl_lr: Option<f64>,
841
842 #[serde(rename = "PNL_NLR")]
843 #[serde(with = "string_as_float_opt")]
844 #[serde(skip_serializing_if = "Option::is_none")]
845 pnl_nlr: Option<f64>,
846
847 #[serde(rename = "FUNDS")]
848 #[serde(with = "string_as_float_opt")]
849 #[serde(skip_serializing_if = "Option::is_none")]
850 funds: Option<f64>,
851
852 #[serde(rename = "MARGIN")]
853 #[serde(with = "string_as_float_opt")]
854 #[serde(skip_serializing_if = "Option::is_none")]
855 margin: Option<f64>,
856
857 #[serde(rename = "MARGIN_LR")]
858 #[serde(with = "string_as_float_opt")]
859 #[serde(skip_serializing_if = "Option::is_none")]
860 margin_lr: Option<f64>,
861
862 #[serde(rename = "MARGIN_NLR")]
863 #[serde(with = "string_as_float_opt")]
864 #[serde(skip_serializing_if = "Option::is_none")]
865 margin_nlr: Option<f64>,
866
867 #[serde(rename = "AVAILABLE_TO_DEAL")]
868 #[serde(with = "string_as_float_opt")]
869 #[serde(skip_serializing_if = "Option::is_none")]
870 available_to_deal: Option<f64>,
871
872 #[serde(rename = "EQUITY")]
873 #[serde(with = "string_as_float_opt")]
874 #[serde(skip_serializing_if = "Option::is_none")]
875 equity: Option<f64>,
876
877 #[serde(rename = "EQUITY_USED")]
878 #[serde(with = "string_as_float_opt")]
879 #[serde(skip_serializing_if = "Option::is_none")]
880 equity_used: Option<f64>,
881}
882
883impl AccountData {
884 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
892 let item_name = item_update.item_name.clone().unwrap_or_default();
894
895 let item_pos = item_update.item_pos as i32;
897
898 let is_snapshot = item_update.is_snapshot;
900
901 let fields = Self::create_account_fields(&item_update.fields)?;
903
904 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
906 for (key, value) in &item_update.changed_fields {
907 changed_fields_map.insert(key.clone(), Some(value.clone()));
908 }
909 let changed_fields = Self::create_account_fields(&changed_fields_map)?;
910
911 Ok(AccountData {
912 item_name,
913 item_pos,
914 fields,
915 changed_fields,
916 is_snapshot,
917 })
918 }
919
920 fn create_account_fields(
928 fields_map: &HashMap<String, Option<String>>,
929 ) -> Result<AccountFields, String> {
930 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
932
933 let parse_float = |key: &str| -> Result<Option<f64>, String> {
935 match get_field(key) {
936 Some(val) if !val.is_empty() => val
937 .parse::<f64>()
938 .map(Some)
939 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
940 _ => Ok(None),
941 }
942 };
943
944 Ok(AccountFields {
945 pnl: parse_float("PNL")?,
946 deposit: parse_float("DEPOSIT")?,
947 available_cash: parse_float("AVAILABLE_CASH")?,
948 pnl_lr: parse_float("PNL_LR")?,
949 pnl_nlr: parse_float("PNL_NLR")?,
950 funds: parse_float("FUNDS")?,
951 margin: parse_float("MARGIN")?,
952 margin_lr: parse_float("MARGIN_LR")?,
953 margin_nlr: parse_float("MARGIN_NLR")?,
954 available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
955 equity: parse_float("EQUITY")?,
956 equity_used: parse_float("EQUITY_USED")?,
957 })
958 }
959}
960
961impl From<&ItemUpdate> for AccountData {
962 fn from(item_update: &ItemUpdate) -> Self {
963 Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::presentation::order::Direction;
971
972 fn sample_position_details(direction: Direction, level: f64, size: f64) -> PositionDetails {
973 PositionDetails {
974 contract_size: 1.0,
975 created_date: "2025/10/30 18:13:53:000".to_string(),
976 created_date_utc: "2025-10-30T17:13:53".to_string(),
977 deal_id: "DIAAAAVJNQPWZAG".to_string(),
978 deal_reference: "RZ0RQ1K8V1S1JN2".to_string(),
979 direction,
980 limit_level: None,
981 level,
982 size,
983 stop_level: None,
984 trailing_step: None,
985 trailing_stop_distance: None,
986 currency: "USD".to_string(),
987 controlled_risk: false,
988 limited_risk_premium: None,
989 }
990 }
991
992 fn sample_market(bid: Option<f64>, offer: Option<f64>) -> PositionMarket {
993 PositionMarket {
994 instrument_name: "US 500 6910 PUT ($1)".to_string(),
995 expiry: "DEC-25".to_string(),
996 epic: "OP.D.OTCSPX3.6910P.IP".to_string(),
997 instrument_type: "UNKNOWN".to_string(),
998 lot_size: 1.0,
999 high: Some(153.43),
1000 low: Some(147.42),
1001 percentage_change: 0.61,
1002 net_change: 6895.38,
1003 bid,
1004 offer,
1005 update_time: "05:55:59".to_string(),
1006 update_time_utc: "05:55:59".to_string(),
1007 delay_time: 0,
1008 streaming_prices_available: true,
1009 market_status: "TRADEABLE".to_string(),
1010 scaling_factor: 1,
1011 }
1012 }
1013
1014 #[test]
1015 fn pnl_sell_uses_offer_and_matches_sample_data() {
1016 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1020 let market = sample_market(Some(151.32), Some(152.82));
1021 let position = Position {
1022 position: details,
1023 market,
1024 pnl: None,
1025 };
1026
1027 let pnl = position.pnl();
1028 assert!((pnl - 2.32).abs() < 1e-9, "expected 2.32, got {}", pnl);
1029 }
1030
1031 #[test]
1032 fn pnl_buy_uses_bid_and_computes_difference() {
1033 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1036 let market = sample_market(Some(151.32), Some(152.82));
1037 let position = Position {
1038 position: details,
1039 market,
1040 pnl: None,
1041 };
1042
1043 let pnl = position.pnl();
1044 assert!((pnl + 3.82).abs() < 1e-9, "expected -3.82, got {}", pnl);
1045 }
1046
1047 #[test]
1048 fn pnl_field_overrides_calculation_when_present() {
1049 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1050 let market = sample_market(Some(151.32), Some(152.82));
1051 let position = Position {
1053 position: details,
1054 market,
1055 pnl: Some(10.0),
1056 };
1057 assert_eq!(position.pnl(), 10.0);
1058 }
1059
1060 #[test]
1061 fn pnl_sell_is_zero_when_offer_missing() {
1062 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1064 let market = sample_market(Some(151.32), None);
1065 let position = Position {
1066 position: details,
1067 market,
1068 pnl: None,
1069 };
1070 assert!((position.pnl() - 0.0).abs() < 1e-12);
1071 }
1072
1073 #[test]
1074 fn pnl_buy_is_zero_when_bid_missing() {
1075 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1077 let market = sample_market(None, Some(152.82));
1078 let position = Position {
1079 position: details,
1080 market,
1081 pnl: None,
1082 };
1083 assert!((position.pnl() - 0.0).abs() < 1e-12);
1084 }
1085}