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 Add for Position {
332 type Output = Position;
333
334 fn add(self, other: Position) -> Position {
335 if self.market.epic != other.market.epic {
336 panic!("Cannot add positions from different markets");
337 }
338 Position {
339 position: self.position + other.position,
340 market: self.market,
341 pnl: match (self.pnl, other.pnl) {
342 (Some(a), Some(b)) => Some(a + b),
343 (Some(a), None) => Some(a),
344 (None, Some(b)) => Some(b),
345 (None, None) => None,
346 },
347 }
348 }
349}
350
351#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
353pub struct PositionDetails {
354 #[serde(rename = "contractSize")]
356 pub contract_size: f64,
357 #[serde(rename = "createdDate")]
359 pub created_date: String,
360 #[serde(rename = "createdDateUTC")]
362 pub created_date_utc: String,
363 #[serde(rename = "dealId")]
365 pub deal_id: String,
366 #[serde(rename = "dealReference")]
368 pub deal_reference: String,
369 pub direction: Direction,
371 #[serde(rename = "limitLevel")]
373 pub limit_level: Option<f64>,
374 pub level: f64,
376 pub size: f64,
378 #[serde(rename = "stopLevel")]
380 pub stop_level: Option<f64>,
381 #[serde(rename = "trailingStep")]
383 pub trailing_step: Option<f64>,
384 #[serde(rename = "trailingStopDistance")]
386 pub trailing_stop_distance: Option<f64>,
387 pub currency: String,
389 #[serde(rename = "controlledRisk")]
391 pub controlled_risk: bool,
392 #[serde(rename = "limitedRiskPremium")]
394 pub limited_risk_premium: Option<f64>,
395}
396
397impl Add for PositionDetails {
398 type Output = PositionDetails;
399
400 fn add(self, other: PositionDetails) -> PositionDetails {
401 let (contract_size, size) = if self.direction != other.direction {
402 (
403 (self.contract_size - other.contract_size).abs(),
404 (self.size - other.size).abs(),
405 )
406 } else {
407 (
408 self.contract_size + other.contract_size,
409 self.size + other.size,
410 )
411 };
412
413 PositionDetails {
414 contract_size,
415 created_date: self.created_date,
416 created_date_utc: self.created_date_utc,
417 deal_id: self.deal_id,
418 deal_reference: self.deal_reference,
419 direction: self.direction,
420 limit_level: other.limit_level.or(self.limit_level),
421 level: (self.level + other.level) / 2.0, size,
423 stop_level: other.stop_level.or(self.stop_level),
424 trailing_step: other.trailing_step.or(self.trailing_step),
425 trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
426 currency: self.currency.clone(),
427 controlled_risk: self.controlled_risk || other.controlled_risk,
428 limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
429 }
430 }
431}
432
433#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
435pub struct PositionMarket {
436 #[serde(rename = "instrumentName")]
438 pub instrument_name: String,
439 pub expiry: String,
441 pub epic: String,
443 #[serde(rename = "instrumentType")]
445 pub instrument_type: String,
446 #[serde(rename = "lotSize")]
448 pub lot_size: f64,
449 pub high: Option<f64>,
451 pub low: Option<f64>,
453 #[serde(rename = "percentageChange")]
455 pub percentage_change: f64,
456 #[serde(rename = "netChange")]
458 pub net_change: f64,
459 pub bid: Option<f64>,
461 pub offer: Option<f64>,
463 #[serde(rename = "updateTime")]
465 pub update_time: String,
466 #[serde(rename = "updateTimeUTC")]
468 pub update_time_utc: String,
469 #[serde(rename = "delayTime")]
471 pub delay_time: i64,
472 #[serde(rename = "streamingPricesAvailable")]
474 pub streaming_prices_available: bool,
475 #[serde(rename = "marketStatus")]
477 pub market_status: String,
478 #[serde(rename = "scalingFactor")]
480 pub scaling_factor: i64,
481}
482
483impl PositionMarket {
484 pub fn is_call(&self) -> bool {
497 self.instrument_name.contains("CALL")
498 }
499
500 pub fn is_put(&self) -> bool {
512 self.instrument_name.contains("PUT")
513 }
514}
515
516#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
518pub struct WorkingOrder {
519 #[serde(rename = "workingOrderData")]
521 pub working_order_data: WorkingOrderData,
522 #[serde(rename = "marketData")]
524 pub market_data: AccountMarketData,
525}
526
527#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
529pub struct WorkingOrderData {
530 #[serde(rename = "dealId")]
532 pub deal_id: String,
533 pub direction: Direction,
535 pub epic: String,
537 #[serde(rename = "orderSize")]
539 pub order_size: f64,
540 #[serde(rename = "orderLevel")]
542 pub order_level: f64,
543 #[serde(rename = "timeInForce")]
545 pub time_in_force: TimeInForce,
546 #[serde(rename = "goodTillDate")]
548 pub good_till_date: Option<String>,
549 #[serde(rename = "goodTillDateISO")]
551 pub good_till_date_iso: Option<String>,
552 #[serde(rename = "createdDate")]
554 pub created_date: String,
555 #[serde(rename = "createdDateUTC")]
557 pub created_date_utc: String,
558 #[serde(rename = "guaranteedStop")]
560 pub guaranteed_stop: bool,
561 #[serde(rename = "orderType")]
563 pub order_type: OrderType,
564 #[serde(rename = "stopDistance")]
566 pub stop_distance: Option<f64>,
567 #[serde(rename = "limitDistance")]
569 pub limit_distance: Option<f64>,
570 #[serde(rename = "currencyCode")]
572 pub currency_code: String,
573 pub dma: bool,
575 #[serde(rename = "limitedRiskPremium")]
577 pub limited_risk_premium: Option<f64>,
578 #[serde(rename = "limitLevel", default)]
580 pub limit_level: Option<f64>,
581 #[serde(rename = "stopLevel", default)]
583 pub stop_level: Option<f64>,
584 #[serde(rename = "dealReference", default)]
586 pub deal_reference: Option<String>,
587}
588
589#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
591pub struct AccountMarketData {
592 #[serde(rename = "instrumentName")]
594 pub instrument_name: String,
595 #[serde(rename = "exchangeId")]
597 pub exchange_id: String,
598 pub expiry: String,
600 #[serde(rename = "marketStatus")]
602 pub market_status: MarketState,
603 pub epic: String,
605 #[serde(rename = "instrumentType")]
607 pub instrument_type: InstrumentType,
608 #[serde(rename = "lotSize")]
610 pub lot_size: f64,
611 pub high: Option<f64>,
613 pub low: Option<f64>,
615 #[serde(rename = "percentageChange")]
617 pub percentage_change: f64,
618 #[serde(rename = "netChange")]
620 pub net_change: f64,
621 pub bid: Option<f64>,
623 pub offer: Option<f64>,
625 #[serde(rename = "updateTime")]
627 pub update_time: String,
628 #[serde(rename = "updateTimeUTC")]
630 pub update_time_utc: String,
631 #[serde(rename = "delayTime")]
633 pub delay_time: i64,
634 #[serde(rename = "streamingPricesAvailable")]
636 pub streaming_prices_available: bool,
637 #[serde(rename = "scalingFactor")]
639 pub scaling_factor: i64,
640}
641
642impl AccountMarketData {
643 pub fn is_call(&self) -> bool {
656 self.instrument_name.contains("CALL")
657 }
658
659 pub fn is_put(&self) -> bool {
671 self.instrument_name.contains("PUT")
672 }
673}
674
675#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
677pub struct TransactionMetadata {
678 #[serde(rename = "pageData")]
680 pub page_data: PageData,
681 pub size: i32,
683}
684
685#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
687pub struct PageData {
688 #[serde(rename = "pageNumber")]
690 pub page_number: i32,
691 #[serde(rename = "pageSize")]
693 pub page_size: i32,
694 #[serde(rename = "totalPages")]
696 pub total_pages: i32,
697}
698
699#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
701pub struct AccountTransaction {
702 pub date: String,
704 #[serde(rename = "dateUtc")]
706 pub date_utc: String,
707 #[serde(rename = "openDateUtc")]
709 pub open_date_utc: String,
710 #[serde(rename = "instrumentName")]
712 pub instrument_name: String,
713 pub period: String,
715 #[serde(rename = "profitAndLoss")]
717 pub profit_and_loss: String,
718 #[serde(rename = "transactionType")]
720 pub transaction_type: String,
721 pub reference: String,
723 #[serde(rename = "openLevel")]
725 pub open_level: String,
726 #[serde(rename = "closeLevel")]
728 pub close_level: String,
729 pub size: String,
731 pub currency: String,
733 #[serde(rename = "cashTransaction")]
735 pub cash_transaction: bool,
736}
737
738impl AccountTransaction {
739 pub fn is_call(&self) -> bool {
752 self.instrument_name.contains("CALL")
753 }
754
755 pub fn is_put(&self) -> bool {
767 self.instrument_name.contains("PUT")
768 }
769}
770
771#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
773pub struct AccountData {
774 pub item_name: String,
776 pub item_pos: i32,
778 pub fields: AccountFields,
780 pub changed_fields: AccountFields,
782 pub is_snapshot: bool,
784}
785
786#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
788pub struct AccountFields {
789 #[serde(rename = "PNL")]
790 #[serde(with = "string_as_float_opt")]
791 #[serde(skip_serializing_if = "Option::is_none")]
792 pnl: Option<f64>,
793
794 #[serde(rename = "DEPOSIT")]
795 #[serde(with = "string_as_float_opt")]
796 #[serde(skip_serializing_if = "Option::is_none")]
797 deposit: Option<f64>,
798
799 #[serde(rename = "AVAILABLE_CASH")]
800 #[serde(with = "string_as_float_opt")]
801 #[serde(skip_serializing_if = "Option::is_none")]
802 available_cash: Option<f64>,
803
804 #[serde(rename = "PNL_LR")]
805 #[serde(with = "string_as_float_opt")]
806 #[serde(skip_serializing_if = "Option::is_none")]
807 pnl_lr: Option<f64>,
808
809 #[serde(rename = "PNL_NLR")]
810 #[serde(with = "string_as_float_opt")]
811 #[serde(skip_serializing_if = "Option::is_none")]
812 pnl_nlr: Option<f64>,
813
814 #[serde(rename = "FUNDS")]
815 #[serde(with = "string_as_float_opt")]
816 #[serde(skip_serializing_if = "Option::is_none")]
817 funds: Option<f64>,
818
819 #[serde(rename = "MARGIN")]
820 #[serde(with = "string_as_float_opt")]
821 #[serde(skip_serializing_if = "Option::is_none")]
822 margin: Option<f64>,
823
824 #[serde(rename = "MARGIN_LR")]
825 #[serde(with = "string_as_float_opt")]
826 #[serde(skip_serializing_if = "Option::is_none")]
827 margin_lr: Option<f64>,
828
829 #[serde(rename = "MARGIN_NLR")]
830 #[serde(with = "string_as_float_opt")]
831 #[serde(skip_serializing_if = "Option::is_none")]
832 margin_nlr: Option<f64>,
833
834 #[serde(rename = "AVAILABLE_TO_DEAL")]
835 #[serde(with = "string_as_float_opt")]
836 #[serde(skip_serializing_if = "Option::is_none")]
837 available_to_deal: Option<f64>,
838
839 #[serde(rename = "EQUITY")]
840 #[serde(with = "string_as_float_opt")]
841 #[serde(skip_serializing_if = "Option::is_none")]
842 equity: Option<f64>,
843
844 #[serde(rename = "EQUITY_USED")]
845 #[serde(with = "string_as_float_opt")]
846 #[serde(skip_serializing_if = "Option::is_none")]
847 equity_used: Option<f64>,
848}
849
850impl AccountData {
851 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
859 let item_name = item_update.item_name.clone().unwrap_or_default();
861
862 let item_pos = item_update.item_pos as i32;
864
865 let is_snapshot = item_update.is_snapshot;
867
868 let fields = Self::create_account_fields(&item_update.fields)?;
870
871 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
873 for (key, value) in &item_update.changed_fields {
874 changed_fields_map.insert(key.clone(), Some(value.clone()));
875 }
876 let changed_fields = Self::create_account_fields(&changed_fields_map)?;
877
878 Ok(AccountData {
879 item_name,
880 item_pos,
881 fields,
882 changed_fields,
883 is_snapshot,
884 })
885 }
886
887 fn create_account_fields(
895 fields_map: &HashMap<String, Option<String>>,
896 ) -> Result<AccountFields, String> {
897 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
899
900 let parse_float = |key: &str| -> Result<Option<f64>, String> {
902 match get_field(key) {
903 Some(val) if !val.is_empty() => val
904 .parse::<f64>()
905 .map(Some)
906 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
907 _ => Ok(None),
908 }
909 };
910
911 Ok(AccountFields {
912 pnl: parse_float("PNL")?,
913 deposit: parse_float("DEPOSIT")?,
914 available_cash: parse_float("AVAILABLE_CASH")?,
915 pnl_lr: parse_float("PNL_LR")?,
916 pnl_nlr: parse_float("PNL_NLR")?,
917 funds: parse_float("FUNDS")?,
918 margin: parse_float("MARGIN")?,
919 margin_lr: parse_float("MARGIN_LR")?,
920 margin_nlr: parse_float("MARGIN_NLR")?,
921 available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
922 equity: parse_float("EQUITY")?,
923 equity_used: parse_float("EQUITY_USED")?,
924 })
925 }
926}
927
928impl From<&ItemUpdate> for AccountData {
929 fn from(item_update: &ItemUpdate) -> Self {
930 Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937 use crate::presentation::order::Direction;
938
939 fn sample_position_details(direction: Direction, level: f64, size: f64) -> PositionDetails {
940 PositionDetails {
941 contract_size: 1.0,
942 created_date: "2025/10/30 18:13:53:000".to_string(),
943 created_date_utc: "2025-10-30T17:13:53".to_string(),
944 deal_id: "DIAAAAVJNQPWZAG".to_string(),
945 deal_reference: "RZ0RQ1K8V1S1JN2".to_string(),
946 direction,
947 limit_level: None,
948 level,
949 size,
950 stop_level: None,
951 trailing_step: None,
952 trailing_stop_distance: None,
953 currency: "USD".to_string(),
954 controlled_risk: false,
955 limited_risk_premium: None,
956 }
957 }
958
959 fn sample_market(bid: Option<f64>, offer: Option<f64>) -> PositionMarket {
960 PositionMarket {
961 instrument_name: "US 500 6910 PUT ($1)".to_string(),
962 expiry: "DEC-25".to_string(),
963 epic: "OP.D.OTCSPX3.6910P.IP".to_string(),
964 instrument_type: "UNKNOWN".to_string(),
965 lot_size: 1.0,
966 high: Some(153.43),
967 low: Some(147.42),
968 percentage_change: 0.61,
969 net_change: 6895.38,
970 bid,
971 offer,
972 update_time: "05:55:59".to_string(),
973 update_time_utc: "05:55:59".to_string(),
974 delay_time: 0,
975 streaming_prices_available: true,
976 market_status: "TRADEABLE".to_string(),
977 scaling_factor: 1,
978 }
979 }
980
981 #[test]
982 fn pnl_sell_uses_offer_and_matches_sample_data() {
983 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
987 let market = sample_market(Some(151.32), Some(152.82));
988 let position = Position {
989 position: details,
990 market,
991 pnl: None,
992 };
993
994 let pnl = position.pnl();
995 assert!((pnl - 2.32).abs() < 1e-9, "expected 2.32, got {}", pnl);
996 }
997
998 #[test]
999 fn pnl_buy_uses_bid_and_computes_difference() {
1000 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1003 let market = sample_market(Some(151.32), Some(152.82));
1004 let position = Position {
1005 position: details,
1006 market,
1007 pnl: None,
1008 };
1009
1010 let pnl = position.pnl();
1011 assert!((pnl + 3.82).abs() < 1e-9, "expected -3.82, got {}", pnl);
1012 }
1013
1014 #[test]
1015 fn pnl_field_overrides_calculation_when_present() {
1016 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1017 let market = sample_market(Some(151.32), Some(152.82));
1018 let position = Position {
1020 position: details,
1021 market,
1022 pnl: Some(10.0),
1023 };
1024 assert_eq!(position.pnl(), 10.0);
1025 }
1026
1027 #[test]
1028 fn pnl_sell_is_zero_when_offer_missing() {
1029 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
1031 let market = sample_market(Some(151.32), None);
1032 let position = Position {
1033 position: details,
1034 market,
1035 pnl: None,
1036 };
1037 assert!((position.pnl() - 0.0).abs() < 1e-12);
1038 }
1039
1040 #[test]
1041 fn pnl_buy_is_zero_when_bid_missing() {
1042 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
1044 let market = sample_market(None, Some(152.82));
1045 let position = Position {
1046 position: details,
1047 market,
1048 pnl: None,
1049 };
1050 assert!((position.pnl() - 0.0).abs() < 1e-12);
1051 }
1052}