ig_client/presentation/
account.rs

1use crate::presentation::serialization::string_as_float_opt;
2use lightstreamer_rs::subscription::ItemUpdate;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
6
7/// Representation of account data received from the IG Markets streaming API
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct AccountData {
10    /// Name of the item this data belongs to
11    item_name: String,
12    /// Position of the item in the subscription
13    item_pos: i32,
14    /// All account fields
15    fields: AccountFields,
16    /// Fields that have changed in this update
17    changed_fields: AccountFields,
18    /// Whether this is a snapshot or an update
19    is_snapshot: bool,
20}
21
22/// Fields containing account financial information
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24pub struct AccountFields {
25    #[serde(rename = "PNL")]
26    #[serde(with = "string_as_float_opt")]
27    #[serde(default)]
28    pnl: Option<f64>,
29
30    #[serde(rename = "DEPOSIT")]
31    #[serde(with = "string_as_float_opt")]
32    #[serde(default)]
33    deposit: Option<f64>,
34
35    #[serde(rename = "AVAILABLE_CASH")]
36    #[serde(with = "string_as_float_opt")]
37    #[serde(default)]
38    available_cash: Option<f64>,
39
40    #[serde(rename = "PNL_LR")]
41    #[serde(with = "string_as_float_opt")]
42    #[serde(default)]
43    pnl_lr: Option<f64>,
44
45    #[serde(rename = "PNL_NLR")]
46    #[serde(with = "string_as_float_opt")]
47    #[serde(default)]
48    pnl_nlr: Option<f64>,
49
50    #[serde(rename = "FUNDS")]
51    #[serde(with = "string_as_float_opt")]
52    #[serde(default)]
53    funds: Option<f64>,
54
55    #[serde(rename = "MARGIN")]
56    #[serde(with = "string_as_float_opt")]
57    #[serde(default)]
58    margin: Option<f64>,
59
60    #[serde(rename = "MARGIN_LR")]
61    #[serde(with = "string_as_float_opt")]
62    #[serde(default)]
63    margin_lr: Option<f64>,
64
65    #[serde(rename = "MARGIN_NLR")]
66    #[serde(with = "string_as_float_opt")]
67    #[serde(default)]
68    margin_nlr: Option<f64>,
69
70    #[serde(rename = "AVAILABLE_TO_DEAL")]
71    #[serde(with = "string_as_float_opt")]
72    #[serde(default)]
73    available_to_deal: Option<f64>,
74
75    #[serde(rename = "EQUITY")]
76    #[serde(with = "string_as_float_opt")]
77    #[serde(default)]
78    equity: Option<f64>,
79
80    #[serde(rename = "EQUITY_USED")]
81    #[serde(with = "string_as_float_opt")]
82    #[serde(default)]
83    equity_used: Option<f64>,
84}
85
86impl AccountData {
87    /// Converts an ItemUpdate from the Lightstreamer API to an AccountData object
88    ///
89    /// # Arguments
90    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
91    ///
92    /// # Returns
93    /// * `Result<Self, String>` - The converted AccountData or an error message
94    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
95        // Extract the item_name, defaulting to an empty string if None
96        let item_name = item_update.item_name.clone().unwrap_or_default();
97
98        // Convert item_pos from usize to i32
99        let item_pos = item_update.item_pos as i32;
100
101        // Extract is_snapshot
102        let is_snapshot = item_update.is_snapshot;
103
104        // Convert fields
105        let fields = Self::create_account_fields(&item_update.fields)?;
106
107        // Convert changed_fields by first creating a HashMap<String, Option<String>>
108        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
109        for (key, value) in &item_update.changed_fields {
110            changed_fields_map.insert(key.clone(), Some(value.clone()));
111        }
112        let changed_fields = Self::create_account_fields(&changed_fields_map)?;
113
114        Ok(AccountData {
115            item_name,
116            item_pos,
117            fields,
118            changed_fields,
119            is_snapshot,
120        })
121    }
122
123    /// Helper method to create AccountFields from a HashMap of field values
124    ///
125    /// # Arguments
126    /// * `fields_map` - HashMap containing field names and their string values
127    ///
128    /// # Returns
129    /// * `Result<AccountFields, String>` - The parsed AccountFields or an error message
130    fn create_account_fields(
131        fields_map: &HashMap<String, Option<String>>,
132    ) -> Result<AccountFields, String> {
133        // Helper function to safely get a field value
134        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
135
136        // Helper function to parse float values
137        let parse_float = |key: &str| -> Result<Option<f64>, String> {
138            match get_field(key) {
139                Some(val) if !val.is_empty() => val
140                    .parse::<f64>()
141                    .map(Some)
142                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
143                _ => Ok(None),
144            }
145        };
146
147        Ok(AccountFields {
148            pnl: parse_float("PNL")?,
149            deposit: parse_float("DEPOSIT")?,
150            available_cash: parse_float("AVAILABLE_CASH")?,
151            pnl_lr: parse_float("PNL_LR")?,
152            pnl_nlr: parse_float("PNL_NLR")?,
153            funds: parse_float("FUNDS")?,
154            margin: parse_float("MARGIN")?,
155            margin_lr: parse_float("MARGIN_LR")?,
156            margin_nlr: parse_float("MARGIN_NLR")?,
157            available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
158            equity: parse_float("EQUITY")?,
159            equity_used: parse_float("EQUITY_USED")?,
160        })
161    }
162}
163
164impl fmt::Display for AccountData {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
167        write!(f, "{json}")
168    }
169}
170
171impl From<&ItemUpdate> for AccountData {
172    fn from(item_update: &ItemUpdate) -> Self {
173        Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
174    }
175}