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