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