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
483#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
485pub struct WorkingOrder {
486 #[serde(rename = "workingOrderData")]
488 pub working_order_data: WorkingOrderData,
489 #[serde(rename = "marketData")]
491 pub market_data: AccountMarketData,
492}
493
494#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
496pub struct WorkingOrderData {
497 #[serde(rename = "dealId")]
499 pub deal_id: String,
500 pub direction: Direction,
502 pub epic: String,
504 #[serde(rename = "orderSize")]
506 pub order_size: f64,
507 #[serde(rename = "orderLevel")]
509 pub order_level: f64,
510 #[serde(rename = "timeInForce")]
512 pub time_in_force: TimeInForce,
513 #[serde(rename = "goodTillDate")]
515 pub good_till_date: Option<String>,
516 #[serde(rename = "goodTillDateISO")]
518 pub good_till_date_iso: Option<String>,
519 #[serde(rename = "createdDate")]
521 pub created_date: String,
522 #[serde(rename = "createdDateUTC")]
524 pub created_date_utc: String,
525 #[serde(rename = "guaranteedStop")]
527 pub guaranteed_stop: bool,
528 #[serde(rename = "orderType")]
530 pub order_type: OrderType,
531 #[serde(rename = "stopDistance")]
533 pub stop_distance: Option<f64>,
534 #[serde(rename = "limitDistance")]
536 pub limit_distance: Option<f64>,
537 #[serde(rename = "currencyCode")]
539 pub currency_code: String,
540 pub dma: bool,
542 #[serde(rename = "limitedRiskPremium")]
544 pub limited_risk_premium: Option<f64>,
545 #[serde(rename = "limitLevel", default)]
547 pub limit_level: Option<f64>,
548 #[serde(rename = "stopLevel", default)]
550 pub stop_level: Option<f64>,
551 #[serde(rename = "dealReference", default)]
553 pub deal_reference: Option<String>,
554}
555
556#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
558pub struct AccountMarketData {
559 #[serde(rename = "instrumentName")]
561 pub instrument_name: String,
562 #[serde(rename = "exchangeId")]
564 pub exchange_id: String,
565 pub expiry: String,
567 #[serde(rename = "marketStatus")]
569 pub market_status: MarketState,
570 pub epic: String,
572 #[serde(rename = "instrumentType")]
574 pub instrument_type: InstrumentType,
575 #[serde(rename = "lotSize")]
577 pub lot_size: f64,
578 pub high: Option<f64>,
580 pub low: Option<f64>,
582 #[serde(rename = "percentageChange")]
584 pub percentage_change: f64,
585 #[serde(rename = "netChange")]
587 pub net_change: f64,
588 pub bid: Option<f64>,
590 pub offer: Option<f64>,
592 #[serde(rename = "updateTime")]
594 pub update_time: String,
595 #[serde(rename = "updateTimeUTC")]
597 pub update_time_utc: String,
598 #[serde(rename = "delayTime")]
600 pub delay_time: i64,
601 #[serde(rename = "streamingPricesAvailable")]
603 pub streaming_prices_available: bool,
604 #[serde(rename = "scalingFactor")]
606 pub scaling_factor: i64,
607}
608
609#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
611pub struct TransactionMetadata {
612 #[serde(rename = "pageData")]
614 pub page_data: PageData,
615 pub size: i32,
617}
618
619#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
621pub struct PageData {
622 #[serde(rename = "pageNumber")]
624 pub page_number: i32,
625 #[serde(rename = "pageSize")]
627 pub page_size: i32,
628 #[serde(rename = "totalPages")]
630 pub total_pages: i32,
631}
632
633#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
635pub struct AccountTransaction {
636 pub date: String,
638 #[serde(rename = "dateUtc")]
640 pub date_utc: String,
641 #[serde(rename = "openDateUtc")]
643 pub open_date_utc: String,
644 #[serde(rename = "instrumentName")]
646 pub instrument_name: String,
647 pub period: String,
649 #[serde(rename = "profitAndLoss")]
651 pub profit_and_loss: String,
652 #[serde(rename = "transactionType")]
654 pub transaction_type: String,
655 pub reference: String,
657 #[serde(rename = "openLevel")]
659 pub open_level: String,
660 #[serde(rename = "closeLevel")]
662 pub close_level: String,
663 pub size: String,
665 pub currency: String,
667 #[serde(rename = "cashTransaction")]
669 pub cash_transaction: bool,
670}
671
672#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
674pub struct AccountData {
675 pub item_name: String,
677 pub item_pos: i32,
679 pub fields: AccountFields,
681 pub changed_fields: AccountFields,
683 pub is_snapshot: bool,
685}
686
687#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
689pub struct AccountFields {
690 #[serde(rename = "PNL")]
691 #[serde(with = "string_as_float_opt")]
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pnl: Option<f64>,
694
695 #[serde(rename = "DEPOSIT")]
696 #[serde(with = "string_as_float_opt")]
697 #[serde(skip_serializing_if = "Option::is_none")]
698 deposit: Option<f64>,
699
700 #[serde(rename = "AVAILABLE_CASH")]
701 #[serde(with = "string_as_float_opt")]
702 #[serde(skip_serializing_if = "Option::is_none")]
703 available_cash: Option<f64>,
704
705 #[serde(rename = "PNL_LR")]
706 #[serde(with = "string_as_float_opt")]
707 #[serde(skip_serializing_if = "Option::is_none")]
708 pnl_lr: Option<f64>,
709
710 #[serde(rename = "PNL_NLR")]
711 #[serde(with = "string_as_float_opt")]
712 #[serde(skip_serializing_if = "Option::is_none")]
713 pnl_nlr: Option<f64>,
714
715 #[serde(rename = "FUNDS")]
716 #[serde(with = "string_as_float_opt")]
717 #[serde(skip_serializing_if = "Option::is_none")]
718 funds: Option<f64>,
719
720 #[serde(rename = "MARGIN")]
721 #[serde(with = "string_as_float_opt")]
722 #[serde(skip_serializing_if = "Option::is_none")]
723 margin: Option<f64>,
724
725 #[serde(rename = "MARGIN_LR")]
726 #[serde(with = "string_as_float_opt")]
727 #[serde(skip_serializing_if = "Option::is_none")]
728 margin_lr: Option<f64>,
729
730 #[serde(rename = "MARGIN_NLR")]
731 #[serde(with = "string_as_float_opt")]
732 #[serde(skip_serializing_if = "Option::is_none")]
733 margin_nlr: Option<f64>,
734
735 #[serde(rename = "AVAILABLE_TO_DEAL")]
736 #[serde(with = "string_as_float_opt")]
737 #[serde(skip_serializing_if = "Option::is_none")]
738 available_to_deal: Option<f64>,
739
740 #[serde(rename = "EQUITY")]
741 #[serde(with = "string_as_float_opt")]
742 #[serde(skip_serializing_if = "Option::is_none")]
743 equity: Option<f64>,
744
745 #[serde(rename = "EQUITY_USED")]
746 #[serde(with = "string_as_float_opt")]
747 #[serde(skip_serializing_if = "Option::is_none")]
748 equity_used: Option<f64>,
749}
750
751impl AccountData {
752 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
760 let item_name = item_update.item_name.clone().unwrap_or_default();
762
763 let item_pos = item_update.item_pos as i32;
765
766 let is_snapshot = item_update.is_snapshot;
768
769 let fields = Self::create_account_fields(&item_update.fields)?;
771
772 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
774 for (key, value) in &item_update.changed_fields {
775 changed_fields_map.insert(key.clone(), Some(value.clone()));
776 }
777 let changed_fields = Self::create_account_fields(&changed_fields_map)?;
778
779 Ok(AccountData {
780 item_name,
781 item_pos,
782 fields,
783 changed_fields,
784 is_snapshot,
785 })
786 }
787
788 fn create_account_fields(
796 fields_map: &HashMap<String, Option<String>>,
797 ) -> Result<AccountFields, String> {
798 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
800
801 let parse_float = |key: &str| -> Result<Option<f64>, String> {
803 match get_field(key) {
804 Some(val) if !val.is_empty() => val
805 .parse::<f64>()
806 .map(Some)
807 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
808 _ => Ok(None),
809 }
810 };
811
812 Ok(AccountFields {
813 pnl: parse_float("PNL")?,
814 deposit: parse_float("DEPOSIT")?,
815 available_cash: parse_float("AVAILABLE_CASH")?,
816 pnl_lr: parse_float("PNL_LR")?,
817 pnl_nlr: parse_float("PNL_NLR")?,
818 funds: parse_float("FUNDS")?,
819 margin: parse_float("MARGIN")?,
820 margin_lr: parse_float("MARGIN_LR")?,
821 margin_nlr: parse_float("MARGIN_NLR")?,
822 available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
823 equity: parse_float("EQUITY")?,
824 equity_used: parse_float("EQUITY_USED")?,
825 })
826 }
827}
828
829impl From<&ItemUpdate> for AccountData {
830 fn from(item_update: &ItemUpdate) -> Self {
831 Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::presentation::order::Direction;
839
840 fn sample_position_details(direction: Direction, level: f64, size: f64) -> PositionDetails {
841 PositionDetails {
842 contract_size: 1.0,
843 created_date: "2025/10/30 18:13:53:000".to_string(),
844 created_date_utc: "2025-10-30T17:13:53".to_string(),
845 deal_id: "DIAAAAVJNQPWZAG".to_string(),
846 deal_reference: "RZ0RQ1K8V1S1JN2".to_string(),
847 direction,
848 limit_level: None,
849 level,
850 size,
851 stop_level: None,
852 trailing_step: None,
853 trailing_stop_distance: None,
854 currency: "USD".to_string(),
855 controlled_risk: false,
856 limited_risk_premium: None,
857 }
858 }
859
860 fn sample_market(bid: Option<f64>, offer: Option<f64>) -> PositionMarket {
861 PositionMarket {
862 instrument_name: "US 500 6910 PUT ($1)".to_string(),
863 expiry: "DEC-25".to_string(),
864 epic: "OP.D.OTCSPX3.6910P.IP".to_string(),
865 instrument_type: "UNKNOWN".to_string(),
866 lot_size: 1.0,
867 high: Some(153.43),
868 low: Some(147.42),
869 percentage_change: 0.61,
870 net_change: 6895.38,
871 bid,
872 offer,
873 update_time: "05:55:59".to_string(),
874 update_time_utc: "05:55:59".to_string(),
875 delay_time: 0,
876 streaming_prices_available: true,
877 market_status: "TRADEABLE".to_string(),
878 scaling_factor: 1,
879 }
880 }
881
882 #[test]
883 fn pnl_sell_uses_offer_and_matches_sample_data() {
884 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
888 let market = sample_market(Some(151.32), Some(152.82));
889 let position = Position {
890 position: details,
891 market,
892 pnl: None,
893 };
894
895 let pnl = position.pnl();
896 assert!((pnl - 2.32).abs() < 1e-9, "expected 2.32, got {}", pnl);
897 }
898
899 #[test]
900 fn pnl_buy_uses_bid_and_computes_difference() {
901 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
904 let market = sample_market(Some(151.32), Some(152.82));
905 let position = Position {
906 position: details,
907 market,
908 pnl: None,
909 };
910
911 let pnl = position.pnl();
912 assert!((pnl + 3.82).abs() < 1e-9, "expected -3.82, got {}", pnl);
913 }
914
915 #[test]
916 fn pnl_field_overrides_calculation_when_present() {
917 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
918 let market = sample_market(Some(151.32), Some(152.82));
919 let position = Position {
921 position: details,
922 market,
923 pnl: Some(10.0),
924 };
925 assert_eq!(position.pnl(), 10.0);
926 }
927
928 #[test]
929 fn pnl_sell_is_zero_when_offer_missing() {
930 let details = sample_position_details(Direction::Sell, 155.14, 1.0);
932 let market = sample_market(Some(151.32), None);
933 let position = Position {
934 position: details,
935 market,
936 pnl: None,
937 };
938 assert!((position.pnl() - 0.0).abs() < 1e-12);
939 }
940
941 #[test]
942 fn pnl_buy_is_zero_when_bid_missing() {
943 let details = sample_position_details(Direction::Buy, 155.14, 1.0);
945 let market = sample_market(None, Some(152.82));
946 let position = Position {
947 position: details,
948 market,
949 pnl: None,
950 };
951 assert!((position.pnl() - 0.0).abs() < 1e-12);
952 }
953}