ig_client/presentation/
account.rs

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/// Account information
12#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
13pub struct AccountInfo {
14    /// List of accounts owned by the user
15    pub accounts: Vec<Account>,
16}
17
18/// Details of a specific account
19#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
20pub struct Account {
21    /// Unique identifier for the account
22    #[serde(rename = "accountId")]
23    pub account_id: String,
24    /// Name of the account
25    #[serde(rename = "accountName")]
26    pub account_name: String,
27    /// Type of the account (e.g., CFD, Spread bet)
28    #[serde(rename = "accountType")]
29    pub account_type: String,
30    /// Balance information for the account
31    pub balance: AccountBalance,
32    /// Base currency of the account
33    pub currency: String,
34    /// Current status of the account
35    pub status: String,
36    /// Whether this is the preferred account
37    pub preferred: bool,
38}
39
40/// Account balance information
41#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
42pub struct AccountBalance {
43    /// Total balance of the account
44    pub balance: f64,
45    /// Deposit amount
46    pub deposit: f64,
47    /// Current profit or loss
48    #[serde(rename = "profitLoss")]
49    pub profit_loss: f64,
50    /// Available funds for trading
51    pub available: f64,
52}
53
54/// Metadata for activity pagination
55#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
56pub struct ActivityMetadata {
57    /// Paging information
58    pub paging: Option<ActivityPaging>,
59}
60
61/// Paging information for activities
62#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
63pub struct ActivityPaging {
64    /// Number of items per page
65    pub size: Option<i32>,
66    /// URL for the next page of results
67    pub next: Option<String>,
68}
69
70#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
71/// Type of account activity
72#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
73pub enum ActivityType {
74    /// Activity related to editing stop and limit orders
75    EditStopAndLimit,
76    /// Activity related to positions
77    Position,
78    /// System-generated activity
79    System,
80    /// Activity related to working orders
81    WorkingOrder,
82}
83
84/// Individual activity record
85#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
86pub struct Activity {
87    /// Date and time of the activity
88    pub date: String,
89    /// Unique identifier for the deal
90    #[serde(rename = "dealId", default)]
91    pub deal_id: Option<String>,
92    /// Instrument EPIC identifier
93    #[serde(default)]
94    pub epic: Option<String>,
95    /// Time period of the activity
96    #[serde(default)]
97    pub period: Option<String>,
98    /// Client-generated reference for the deal
99    #[serde(rename = "dealReference", default)]
100    pub deal_reference: Option<String>,
101    /// Type of activity
102    #[serde(rename = "type")]
103    pub activity_type: ActivityType,
104    /// Status of the activity
105    #[serde(default)]
106    pub status: Option<Status>,
107    /// Description of the activity
108    #[serde(default)]
109    pub description: Option<String>,
110    /// Additional details about the activity
111    /// This is a string when detailed=false, and an object when detailed=true
112    #[serde(default)]
113    pub details: Option<ActivityDetails>,
114    /// Channel the activity occurred on (e.g., "WEB" or "Mobile")
115    #[serde(default)]
116    pub channel: Option<String>,
117    /// The currency, e.g., a pound symbol
118    #[serde(default)]
119    pub currency: Option<String>,
120    /// Price level
121    #[serde(default)]
122    pub level: Option<String>,
123}
124
125/// Detailed information about an activity
126/// Only available when using the detailed=true parameter
127#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
128pub struct ActivityDetails {
129    /// Client-generated reference for the deal
130    #[serde(rename = "dealReference", default)]
131    pub deal_reference: Option<String>,
132    /// List of actions associated with this activity
133    #[serde(default)]
134    pub actions: Vec<ActivityAction>,
135    /// Name of the market
136    #[serde(rename = "marketName", default)]
137    pub market_name: Option<String>,
138    /// Date until which the order is valid
139    #[serde(rename = "goodTillDate", default)]
140    pub good_till_date: Option<String>,
141    /// Currency of the transaction
142    #[serde(default)]
143    pub currency: Option<String>,
144    /// Size/quantity of the transaction
145    #[serde(default)]
146    pub size: Option<f64>,
147    /// Direction of the transaction (BUY or SELL)
148    #[serde(default)]
149    pub direction: Option<Direction>,
150    /// Price level
151    #[serde(default)]
152    pub level: Option<f64>,
153    /// Stop level price
154    #[serde(rename = "stopLevel", default)]
155    pub stop_level: Option<f64>,
156    /// Distance for the stop
157    #[serde(rename = "stopDistance", default)]
158    pub stop_distance: Option<f64>,
159    /// Whether the stop is guaranteed
160    #[serde(rename = "guaranteedStop", default)]
161    pub guaranteed_stop: Option<bool>,
162    /// Distance for the trailing stop
163    #[serde(rename = "trailingStopDistance", default)]
164    pub trailing_stop_distance: Option<f64>,
165    /// Step size for the trailing stop
166    #[serde(rename = "trailingStep", default)]
167    pub trailing_step: Option<f64>,
168    /// Limit level price
169    #[serde(rename = "limitLevel", default)]
170    pub limit_level: Option<f64>,
171    /// Distance for the limit
172    #[serde(rename = "limitDistance", default)]
173    pub limit_distance: Option<f64>,
174}
175
176/// Types of actions that can be performed on an activity
177#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
178#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
179pub enum ActionType {
180    /// A limit order was deleted
181    LimitOrderDeleted,
182    /// A limit order was filled
183    LimitOrderFilled,
184    /// A limit order was opened
185    LimitOrderOpened,
186    /// A limit order was rolled
187    LimitOrderRolled,
188    /// A position was closed
189    PositionClosed,
190    /// A position was deleted
191    PositionDeleted,
192    /// A position was opened
193    PositionOpened,
194    /// A position was partially closed
195    PositionPartiallyClosed,
196    /// A position was rolled
197    PositionRolled,
198    /// A stop/limit was amended
199    StopLimitAmended,
200    /// A stop order was amended
201    StopOrderAmended,
202    /// A stop order was deleted
203    StopOrderDeleted,
204    /// A stop order was filled
205    StopOrderFilled,
206    /// A stop order was opened
207    StopOrderOpened,
208    /// A stop order was rolled
209    StopOrderRolled,
210    /// Unknown action type
211    Unknown,
212    /// A working order was deleted
213    WorkingOrderDeleted,
214}
215
216/// Action associated with an activity
217#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
218#[serde(rename_all = "camelCase")]
219pub struct ActivityAction {
220    /// Type of action
221    pub action_type: ActionType,
222    /// Deal ID affected by this action
223    pub affected_deal_id: Option<String>,
224}
225
226/// Individual position
227#[derive(DebugPretty, Clone, DisplaySimple, Serialize, Deserialize)]
228pub struct Position {
229    /// Details of the position
230    pub position: PositionDetails,
231    /// Market information for the position
232    pub market: PositionMarket,
233    /// Profit and loss for the position
234    pub pnl: Option<f64>,
235}
236
237impl Add for Position {
238    type Output = Position;
239
240    fn add(self, other: Position) -> Position {
241        if self.market.epic != other.market.epic {
242            panic!("Cannot add positions from different markets");
243        }
244        Position {
245            position: self.position + other.position,
246            market: self.market,
247            pnl: match (self.pnl, other.pnl) {
248                (Some(a), Some(b)) => Some(a + b),
249                (Some(a), None) => Some(a),
250                (None, Some(b)) => Some(b),
251                (None, None) => None,
252            },
253        }
254    }
255}
256
257/// Details of a position
258#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
259pub struct PositionDetails {
260    /// Size of one contract
261    #[serde(rename = "contractSize")]
262    pub contract_size: f64,
263    /// Date and time when the position was created
264    #[serde(rename = "createdDate")]
265    pub created_date: String,
266    /// UTC date and time when the position was created
267    #[serde(rename = "createdDateUTC")]
268    pub created_date_utc: String,
269    /// Unique identifier for the deal
270    #[serde(rename = "dealId")]
271    pub deal_id: String,
272    /// Client-generated reference for the deal
273    #[serde(rename = "dealReference")]
274    pub deal_reference: String,
275    /// Direction of the position (buy or sell)
276    pub direction: Direction,
277    /// Price level for take profit
278    #[serde(rename = "limitLevel")]
279    pub limit_level: Option<f64>,
280    /// Opening price level of the position
281    pub level: f64,
282    /// Size/quantity of the position
283    pub size: f64,
284    /// Price level for stop loss
285    #[serde(rename = "stopLevel")]
286    pub stop_level: Option<f64>,
287    /// Step size for trailing stop
288    #[serde(rename = "trailingStep")]
289    pub trailing_step: Option<f64>,
290    /// Distance for trailing stop
291    #[serde(rename = "trailingStopDistance")]
292    pub trailing_stop_distance: Option<f64>,
293    /// Currency of the position
294    pub currency: String,
295    /// Whether the position has controlled risk
296    #[serde(rename = "controlledRisk")]
297    pub controlled_risk: bool,
298    /// Premium paid for limited risk
299    #[serde(rename = "limitedRiskPremium")]
300    pub limited_risk_premium: Option<f64>,
301}
302
303impl Add for PositionDetails {
304    type Output = PositionDetails;
305
306    fn add(self, other: PositionDetails) -> PositionDetails {
307        let (contract_size, size) = if self.direction != other.direction {
308            (
309                (self.contract_size - other.contract_size).abs(),
310                (self.size - other.size).abs(),
311            )
312        } else {
313            (
314                self.contract_size + other.contract_size,
315                self.size + other.size,
316            )
317        };
318
319        PositionDetails {
320            contract_size,
321            created_date: self.created_date,
322            created_date_utc: self.created_date_utc,
323            deal_id: self.deal_id,
324            deal_reference: self.deal_reference,
325            direction: self.direction,
326            limit_level: other.limit_level.or(self.limit_level),
327            level: (self.level + other.level) / 2.0, // Average level
328            size,
329            stop_level: other.stop_level.or(self.stop_level),
330            trailing_step: other.trailing_step.or(self.trailing_step),
331            trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
332            currency: self.currency.clone(),
333            controlled_risk: self.controlled_risk || other.controlled_risk,
334            limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
335        }
336    }
337}
338
339/// Market information for a position
340#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
341pub struct PositionMarket {
342    /// Human-readable name of the instrument
343    #[serde(rename = "instrumentName")]
344    pub instrument_name: String,
345    /// Expiry date of the instrument
346    pub expiry: String,
347    /// Unique identifier for the market
348    pub epic: String,
349    /// Type of the instrument
350    #[serde(rename = "instrumentType")]
351    pub instrument_type: String,
352    /// Size of one lot
353    #[serde(rename = "lotSize")]
354    pub lot_size: f64,
355    /// Highest price of the current trading session
356    pub high: Option<f64>,
357    /// Lowest price of the current trading session
358    pub low: Option<f64>,
359    /// Percentage change in price since previous close
360    #[serde(rename = "percentageChange")]
361    pub percentage_change: f64,
362    /// Net change in price since previous close
363    #[serde(rename = "netChange")]
364    pub net_change: f64,
365    /// Current bid price
366    pub bid: Option<f64>,
367    /// Current offer/ask price
368    pub offer: Option<f64>,
369    /// Time of the last price update
370    #[serde(rename = "updateTime")]
371    pub update_time: String,
372    /// UTC time of the last price update
373    #[serde(rename = "updateTimeUTC")]
374    pub update_time_utc: String,
375    /// Delay time in milliseconds for market data
376    #[serde(rename = "delayTime")]
377    pub delay_time: i64,
378    /// Whether streaming prices are available for this market
379    #[serde(rename = "streamingPricesAvailable")]
380    pub streaming_prices_available: bool,
381    /// Current status of the market (e.g., "OPEN", "CLOSED")
382    #[serde(rename = "marketStatus")]
383    pub market_status: String,
384    /// Factor for scaling prices
385    #[serde(rename = "scalingFactor")]
386    pub scaling_factor: i64,
387}
388
389/// Working order
390#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
391pub struct WorkingOrder {
392    /// Details of the working order
393    #[serde(rename = "workingOrderData")]
394    pub working_order_data: WorkingOrderData,
395    /// Market information for the working order
396    #[serde(rename = "marketData")]
397    pub market_data: AccountMarketData,
398}
399
400/// Details of a working order
401#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
402pub struct WorkingOrderData {
403    /// Unique identifier for the deal
404    #[serde(rename = "dealId")]
405    pub deal_id: String,
406    /// Direction of the order (buy or sell)
407    pub direction: Direction,
408    /// Instrument EPIC identifier
409    pub epic: String,
410    /// Size/quantity of the order
411    #[serde(rename = "orderSize")]
412    pub order_size: f64,
413    /// Price level for the order
414    #[serde(rename = "orderLevel")]
415    pub order_level: f64,
416    /// Time in force for the order
417    #[serde(rename = "timeInForce")]
418    pub time_in_force: TimeInForce,
419    /// Expiry date for GTD orders
420    #[serde(rename = "goodTillDate")]
421    pub good_till_date: Option<String>,
422    /// ISO formatted expiry date for GTD orders
423    #[serde(rename = "goodTillDateISO")]
424    pub good_till_date_iso: Option<String>,
425    /// Date and time when the order was created
426    #[serde(rename = "createdDate")]
427    pub created_date: String,
428    /// UTC date and time when the order was created
429    #[serde(rename = "createdDateUTC")]
430    pub created_date_utc: String,
431    /// Whether the order has a guaranteed stop
432    #[serde(rename = "guaranteedStop")]
433    pub guaranteed_stop: bool,
434    /// Type of the order
435    #[serde(rename = "orderType")]
436    pub order_type: OrderType,
437    /// Distance for stop loss
438    #[serde(rename = "stopDistance")]
439    pub stop_distance: Option<f64>,
440    /// Distance for take profit
441    #[serde(rename = "limitDistance")]
442    pub limit_distance: Option<f64>,
443    /// Currency code for the order
444    #[serde(rename = "currencyCode")]
445    pub currency_code: String,
446    /// Whether direct market access is enabled
447    pub dma: bool,
448    /// Premium for limited risk
449    #[serde(rename = "limitedRiskPremium")]
450    pub limited_risk_premium: Option<f64>,
451    /// Price level for take profit
452    #[serde(rename = "limitLevel", default)]
453    pub limit_level: Option<f64>,
454    /// Price level for stop loss
455    #[serde(rename = "stopLevel", default)]
456    pub stop_level: Option<f64>,
457    /// Client-generated reference for the deal
458    #[serde(rename = "dealReference", default)]
459    pub deal_reference: Option<String>,
460}
461
462/// Market data for a working order
463#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
464pub struct AccountMarketData {
465    /// Human-readable name of the instrument
466    #[serde(rename = "instrumentName")]
467    pub instrument_name: String,
468    /// Exchange identifier
469    #[serde(rename = "exchangeId")]
470    pub exchange_id: String,
471    /// Expiry date of the instrument
472    pub expiry: String,
473    /// Current status of the market
474    #[serde(rename = "marketStatus")]
475    pub market_status: MarketState,
476    /// Unique identifier for the market
477    pub epic: String,
478    /// Type of the instrument
479    #[serde(rename = "instrumentType")]
480    pub instrument_type: InstrumentType,
481    /// Size of one lot
482    #[serde(rename = "lotSize")]
483    pub lot_size: f64,
484    /// Highest price of the current trading session
485    pub high: Option<f64>,
486    /// Lowest price of the current trading session
487    pub low: Option<f64>,
488    /// Percentage change in price since previous close
489    #[serde(rename = "percentageChange")]
490    pub percentage_change: f64,
491    /// Net change in price since previous close
492    #[serde(rename = "netChange")]
493    pub net_change: f64,
494    /// Current bid price
495    pub bid: Option<f64>,
496    /// Current offer/ask price
497    pub offer: Option<f64>,
498    /// Time of the last price update
499    #[serde(rename = "updateTime")]
500    pub update_time: String,
501    /// UTC time of the last price update
502    #[serde(rename = "updateTimeUTC")]
503    pub update_time_utc: String,
504    /// Delay time in milliseconds for market data
505    #[serde(rename = "delayTime")]
506    pub delay_time: i64,
507    /// Whether streaming prices are available for this market
508    #[serde(rename = "streamingPricesAvailable")]
509    pub streaming_prices_available: bool,
510    /// Factor for scaling prices
511    #[serde(rename = "scalingFactor")]
512    pub scaling_factor: i64,
513}
514
515/// Transaction metadata
516#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
517pub struct TransactionMetadata {
518    /// Pagination information
519    #[serde(rename = "pageData")]
520    pub page_data: PageData,
521    /// Total number of transactions
522    pub size: i32,
523}
524
525/// Pagination information
526#[derive(DebugPretty, Clone, DisplaySimple, Deserialize, Serialize)]
527pub struct PageData {
528    /// Current page number
529    #[serde(rename = "pageNumber")]
530    pub page_number: i32,
531    /// Number of items per page
532    #[serde(rename = "pageSize")]
533    pub page_size: i32,
534    /// Total number of pages
535    #[serde(rename = "totalPages")]
536    pub total_pages: i32,
537}
538
539/// Individual transaction
540#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
541pub struct AccountTransaction {
542    /// Date and time of the transaction
543    pub date: String,
544    /// UTC date and time of the transaction
545    #[serde(rename = "dateUtc")]
546    pub date_utc: String,
547    /// Represents the date and time in UTC when an event or entity was opened or initiated.
548    #[serde(rename = "openDateUtc")]
549    pub open_date_utc: String,
550    /// Name of the instrument
551    #[serde(rename = "instrumentName")]
552    pub instrument_name: String,
553    /// Time period of the transaction
554    pub period: String,
555    /// Profit or loss amount
556    #[serde(rename = "profitAndLoss")]
557    pub profit_and_loss: String,
558    /// Type of transaction
559    #[serde(rename = "transactionType")]
560    pub transaction_type: String,
561    /// Reference identifier for the transaction
562    pub reference: String,
563    /// Opening price level
564    #[serde(rename = "openLevel")]
565    pub open_level: String,
566    /// Closing price level
567    #[serde(rename = "closeLevel")]
568    pub close_level: String,
569    /// Size/quantity of the transaction
570    pub size: String,
571    /// Currency of the transaction
572    pub currency: String,
573    /// Whether this is a cash transaction
574    #[serde(rename = "cashTransaction")]
575    pub cash_transaction: bool,
576}
577
578/// Representation of account data received from the IG Markets streaming API
579#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
580pub struct AccountData {
581    /// Name of the item this data belongs to
582    pub item_name: String,
583    /// Position of the item in the subscription
584    pub item_pos: i32,
585    /// All account fields
586    pub fields: AccountFields,
587    /// Fields that have changed in this update
588    pub changed_fields: AccountFields,
589    /// Whether this is a snapshot or an update
590    pub is_snapshot: bool,
591}
592
593/// Fields containing account financial information
594#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
595pub struct AccountFields {
596    #[serde(rename = "PNL")]
597    #[serde(with = "string_as_float_opt")]
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pnl: Option<f64>,
600
601    #[serde(rename = "DEPOSIT")]
602    #[serde(with = "string_as_float_opt")]
603    #[serde(skip_serializing_if = "Option::is_none")]
604    deposit: Option<f64>,
605
606    #[serde(rename = "AVAILABLE_CASH")]
607    #[serde(with = "string_as_float_opt")]
608    #[serde(skip_serializing_if = "Option::is_none")]
609    available_cash: Option<f64>,
610
611    #[serde(rename = "PNL_LR")]
612    #[serde(with = "string_as_float_opt")]
613    #[serde(skip_serializing_if = "Option::is_none")]
614    pnl_lr: Option<f64>,
615
616    #[serde(rename = "PNL_NLR")]
617    #[serde(with = "string_as_float_opt")]
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pnl_nlr: Option<f64>,
620
621    #[serde(rename = "FUNDS")]
622    #[serde(with = "string_as_float_opt")]
623    #[serde(skip_serializing_if = "Option::is_none")]
624    funds: Option<f64>,
625
626    #[serde(rename = "MARGIN")]
627    #[serde(with = "string_as_float_opt")]
628    #[serde(skip_serializing_if = "Option::is_none")]
629    margin: Option<f64>,
630
631    #[serde(rename = "MARGIN_LR")]
632    #[serde(with = "string_as_float_opt")]
633    #[serde(skip_serializing_if = "Option::is_none")]
634    margin_lr: Option<f64>,
635
636    #[serde(rename = "MARGIN_NLR")]
637    #[serde(with = "string_as_float_opt")]
638    #[serde(skip_serializing_if = "Option::is_none")]
639    margin_nlr: Option<f64>,
640
641    #[serde(rename = "AVAILABLE_TO_DEAL")]
642    #[serde(with = "string_as_float_opt")]
643    #[serde(skip_serializing_if = "Option::is_none")]
644    available_to_deal: Option<f64>,
645
646    #[serde(rename = "EQUITY")]
647    #[serde(with = "string_as_float_opt")]
648    #[serde(skip_serializing_if = "Option::is_none")]
649    equity: Option<f64>,
650
651    #[serde(rename = "EQUITY_USED")]
652    #[serde(with = "string_as_float_opt")]
653    #[serde(skip_serializing_if = "Option::is_none")]
654    equity_used: Option<f64>,
655}
656
657impl AccountData {
658    /// Converts an ItemUpdate from the Lightstreamer API to an AccountData object
659    ///
660    /// # Arguments
661    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
662    ///
663    /// # Returns
664    /// * `Result<Self, String>` - The converted AccountData or an error message
665    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
666        // Extract the item_name, defaulting to an empty string if None
667        let item_name = item_update.item_name.clone().unwrap_or_default();
668
669        // Convert item_pos from usize to i32
670        let item_pos = item_update.item_pos as i32;
671
672        // Extract is_snapshot
673        let is_snapshot = item_update.is_snapshot;
674
675        // Convert fields
676        let fields = Self::create_account_fields(&item_update.fields)?;
677
678        // Convert changed_fields by first creating a HashMap<String, Option<String>>
679        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
680        for (key, value) in &item_update.changed_fields {
681            changed_fields_map.insert(key.clone(), Some(value.clone()));
682        }
683        let changed_fields = Self::create_account_fields(&changed_fields_map)?;
684
685        Ok(AccountData {
686            item_name,
687            item_pos,
688            fields,
689            changed_fields,
690            is_snapshot,
691        })
692    }
693
694    /// Helper method to create AccountFields from a HashMap of field values
695    ///
696    /// # Arguments
697    /// * `fields_map` - HashMap containing field names and their string values
698    ///
699    /// # Returns
700    /// * `Result<AccountFields, String>` - The parsed AccountFields or an error message
701    fn create_account_fields(
702        fields_map: &HashMap<String, Option<String>>,
703    ) -> Result<AccountFields, String> {
704        // Helper function to safely get a field value
705        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
706
707        // Helper function to parse float values
708        let parse_float = |key: &str| -> Result<Option<f64>, String> {
709            match get_field(key) {
710                Some(val) if !val.is_empty() => val
711                    .parse::<f64>()
712                    .map(Some)
713                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
714                _ => Ok(None),
715            }
716        };
717
718        Ok(AccountFields {
719            pnl: parse_float("PNL")?,
720            deposit: parse_float("DEPOSIT")?,
721            available_cash: parse_float("AVAILABLE_CASH")?,
722            pnl_lr: parse_float("PNL_LR")?,
723            pnl_nlr: parse_float("PNL_NLR")?,
724            funds: parse_float("FUNDS")?,
725            margin: parse_float("MARGIN")?,
726            margin_lr: parse_float("MARGIN_LR")?,
727            margin_nlr: parse_float("MARGIN_NLR")?,
728            available_to_deal: parse_float("AVAILABLE_TO_DEAL")?,
729            equity: parse_float("EQUITY")?,
730            equity_used: parse_float("EQUITY_USED")?,
731        })
732    }
733}
734
735impl From<&ItemUpdate> for AccountData {
736    fn from(item_update: &ItemUpdate) -> Self {
737        Self::from_item_update(item_update).unwrap_or_else(|_| AccountData::default())
738    }
739}