ig_client/model/
streaming.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 25/10/25
5******************************************************************************/
6
7//! Streaming data field definitions for IG Markets API.
8//!
9//! This module provides enums and helper functions for working with streaming
10//! subscriptions in the IG Markets API. It includes field definitions for:
11//! - Market data (prices, market state)
12//! - Price data (detailed bid/ask levels)
13//! - Account data (P&L, margin, equity)
14
15use crate::prelude::{Deserialize, Serialize};
16use std::collections::HashSet;
17use std::fmt::{Debug, Display};
18
19/// Streaming market fields available for market subscriptions.
20///
21/// These fields represent the various market data points that can be subscribed to
22/// in the IG Markets streaming API for market updates.
23#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
24#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
25pub enum StreamingMarketField {
26    /// Mid open price
27    MidOpen,
28    /// High price
29    High,
30    /// Low price
31    Low,
32    /// Price change
33    Change,
34    /// Percentage change
35    ChangePct,
36    /// Last update time
37    UpdateTime,
38    /// Market delay in milliseconds
39    MarketDelay,
40    /// Market state (e.g., TRADEABLE, CLOSED)
41    MarketState,
42    /// Bid price
43    Bid,
44    /// Offer/Ask price
45    #[default]
46    Offer,
47}
48
49impl Debug for StreamingMarketField {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        let field_name = match self {
52            StreamingMarketField::MidOpen => "MID_OPEN",
53            StreamingMarketField::High => "HIGH",
54            StreamingMarketField::Low => "LOW",
55            StreamingMarketField::Change => "CHANGE",
56            StreamingMarketField::ChangePct => "CHANGE_PCT",
57            StreamingMarketField::UpdateTime => "UPDATE_TIME",
58            StreamingMarketField::MarketDelay => "MARKET_DELAY",
59            StreamingMarketField::MarketState => "MARKET_STATE",
60            StreamingMarketField::Bid => "BID",
61            StreamingMarketField::Offer => "OFFER",
62        };
63        write!(f, "{}", field_name)
64    }
65}
66
67impl Display for StreamingMarketField {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{:?}", self)
70    }
71}
72
73/// Constructs a vector of serialized streaming market field names from a given set of `StreamingMarketField`.
74///
75/// # Arguments
76///
77/// * `fields` - A reference to a `HashSet` containing `StreamingMarketField` items that need to be serialized.
78///
79/// # Returns
80///
81/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingMarketField` from the input set.
82///
83/// # Panics
84///
85/// This function will panic if the serialization of any `StreamingMarketField` fails.
86///
87pub(crate) fn get_streaming_market_fields(fields: &HashSet<StreamingMarketField>) -> Vec<String> {
88    let mut fields_vec = Vec::new();
89    for field in fields {
90        // Serialize to a JSON value and extract the underlying string without quotes
91        let val = serde_json::to_value(field).expect("Failed to serialize StreamingMarketField");
92        match val {
93            serde_json::Value::String(s) => fields_vec.push(s),
94            // Fallback: use Debug which yields SCREAMING_SNAKE_CASE variant name
95            _ => fields_vec.push(format!("{:?}", field)),
96        }
97    }
98    fields_vec
99}
100
101/// Streaming price fields available for price subscriptions.
102///
103/// These fields represent the various price data points that can be subscribed to
104/// in the IG Markets streaming API for price updates.
105#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
106#[serde(rename_all = "UPPERCASE")]
107pub enum StreamingPriceField {
108    /// Mid open price
109    #[serde(rename = "MID_OPEN")]
110    MidOpen,
111    /// High price
112    High,
113    /// Low price
114    Low,
115    /// Bid quote ID
116    BidQuoteId,
117    /// Ask quote ID
118    AskQuoteId,
119    /// Bid price level 1
120    BidPrice1,
121    /// Bid price level 2
122    BidPrice2,
123    /// Bid price level 3
124    BidPrice3,
125    /// Bid price level 4
126    BidPrice4,
127    /// Bid price level 5
128    BidPrice5,
129    /// Ask price level 1
130    AskPrice1,
131    /// Ask price level 2
132    AskPrice2,
133    /// Ask price level 3
134    AskPrice3,
135    /// Ask price level 4
136    AskPrice4,
137    /// Ask price level 5
138    #[default]
139    AskPrice5,
140    /// Bid size level 1
141    BidSize1,
142    /// Bid size level 2
143    BidSize2,
144    /// Bid size level 3
145    BidSize3,
146    /// Bid size level 4
147    BidSize4,
148    /// Bid size level 5
149    BidSize5,
150    /// Ask size level 1
151    AskSize1,
152    /// Ask size level 2
153    AskSize2,
154    /// Ask size level 3
155    AskSize3,
156    /// Ask size level 4
157    AskSize4,
158    /// Ask size level 5
159    AskSize5,
160    /// Currency 0
161    Currency0,
162    /// Currency 1
163    Currency1,
164    /// Currency 1 bid size level 1
165    C1BidSize1,
166    /// Currency 1 bid size level 2
167    C1BidSize2,
168    /// Currency 1 bid size level 3
169    C1BidSize3,
170    /// Currency 1 bid size level 4
171    C1BidSize4,
172    /// Currency 1 bid size level 5
173    C1BidSize5,
174    /// Currency 1 ask size level 1
175    C1AskSize1,
176    /// Currency 1 ask size level 2
177    C1AskSize2,
178    /// Currency 1 ask size level 3
179    C1AskSize3,
180    /// Currency 1 ask size level 4
181    C1AskSize4,
182    /// Currency 1 ask size level 5
183    C1AskSize5,
184    /// Currency 2
185    Currency2,
186    /// Currency 2 bid size level 1
187    C2BidSize1,
188    /// Currency 2 bid size level 2
189    C2BidSize2,
190    /// Currency 2 bid size level 3
191    C2BidSize3,
192    /// Currency 2 bid size level 4
193    C2BidSize4,
194    /// Currency 2 bid size level 5
195    C2BidSize5,
196    /// Currency 2 ask size level 1
197    C2AskSize1,
198    /// Currency 2 ask size level 2
199    C2AskSize2,
200    /// Currency 2 ask size level 3
201    C2AskSize3,
202    /// Currency 2 ask size level 4
203    C2AskSize4,
204    /// Currency 2 ask size level 5
205    C2AskSize5,
206    /// Currency 3
207    Currency3,
208    /// Currency 3 bid size level 1
209    C3BidSize1,
210    /// Currency 3 bid size level 2
211    C3BidSize2,
212    /// Currency 3 bid size level 3
213    C3BidSize3,
214    /// Currency 3 bid size level 4
215    C3BidSize4,
216    /// Currency 3 bid size level 5
217    C3BidSize5,
218    /// Currency 3 ask size level 1
219    C3AskSize1,
220    /// Currency 3 ask size level 2
221    C3AskSize2,
222    /// Currency 3 ask size level 3
223    C3AskSize3,
224    /// Currency 3 ask size level 4
225    C3AskSize4,
226    /// Currency 3 ask size level 5
227    C3AskSize5,
228    /// Currency 4
229    Currency4,
230    /// Currency 4 bid size level 1
231    C4BidSize1,
232    /// Currency 4 bid size level 2
233    C4BidSize2,
234    /// Currency 4 bid size level 3
235    C4BidSize3,
236    /// Currency 4 bid size level 4
237    C4BidSize4,
238    /// Currency 4 bid size level 5
239    C4BidSize5,
240    /// Currency 4 ask size level 1
241    C4AskSize1,
242    /// Currency 4 ask size level 2
243    C4AskSize2,
244    /// Currency 4 ask size level 3
245    C4AskSize3,
246    /// Currency 4 ask size level 4
247    C4AskSize4,
248    /// Currency 4 ask size level 5
249    C4AskSize5,
250    /// Currency 5
251    Currency5,
252    /// Currency 5 bid size level 1
253    C5BidSize1,
254    /// Currency 5 bid size level 2
255    C5BidSize2,
256    /// Currency 5 bid size level 3
257    C5BidSize3,
258    /// Currency 5 bid size level 4
259    C5BidSize4,
260    /// Currency 5 bid size level 5
261    C5BidSize5,
262    /// Currency 5 ask size level 1
263    C5AskSize1,
264    /// Currency 5 ask size level 2
265    C5AskSize2,
266    /// Currency 5 ask size level 3
267    C5AskSize3,
268    /// Currency 5 ask size level 4
269    C5AskSize4,
270    /// Currency 5 ask size level 5
271    C5AskSize5,
272    /// Timestamp of the price update
273    Timestamp,
274    /// Dealing flag
275    #[serde(rename = "DLG_FLAG")]
276    DlgFlag,
277}
278
279impl Debug for StreamingPriceField {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        let field_name = match self {
282            StreamingPriceField::MidOpen => "MID_OPEN",
283            StreamingPriceField::High => "HIGH",
284            StreamingPriceField::Low => "LOW",
285            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
286            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
287            StreamingPriceField::BidPrice1 => "BIDPRICE1",
288            StreamingPriceField::BidPrice2 => "BIDPRICE2",
289            StreamingPriceField::BidPrice3 => "BIDPRICE3",
290            StreamingPriceField::BidPrice4 => "BIDPRICE4",
291            StreamingPriceField::BidPrice5 => "BIDPRICE5",
292            StreamingPriceField::AskPrice1 => "ASKPRICE1",
293            StreamingPriceField::AskPrice2 => "ASKPRICE2",
294            StreamingPriceField::AskPrice3 => "ASKPRICE3",
295            StreamingPriceField::AskPrice4 => "ASKPRICE4",
296            StreamingPriceField::AskPrice5 => "ASKPRICE5",
297            StreamingPriceField::BidSize1 => "BIDSIZE1",
298            StreamingPriceField::BidSize2 => "BIDSIZE2",
299            StreamingPriceField::BidSize3 => "BIDSIZE3",
300            StreamingPriceField::BidSize4 => "BIDSIZE4",
301            StreamingPriceField::BidSize5 => "BIDSIZE5",
302            StreamingPriceField::AskSize1 => "ASKSIZE1",
303            StreamingPriceField::AskSize2 => "ASKSIZE2",
304            StreamingPriceField::AskSize3 => "ASKSIZE3",
305            StreamingPriceField::AskSize4 => "ASKSIZE4",
306            StreamingPriceField::AskSize5 => "ASKSIZE5",
307            StreamingPriceField::Currency0 => "CURRENCY0",
308            StreamingPriceField::Currency1 => "CURRENCY1",
309            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
310            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
311            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
312            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
313            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
314            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
315            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
316            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
317            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
318            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
319            StreamingPriceField::Currency2 => "CURRENCY2",
320            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
321            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
322            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
323            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
324            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
325            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
326            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
327            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
328            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
329            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
330            StreamingPriceField::Currency3 => "CURRENCY3",
331            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
332            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
333            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
334            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
335            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
336            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
337            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
338            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
339            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
340            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
341            StreamingPriceField::Currency4 => "CURRENCY4",
342            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
343            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
344            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
345            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
346            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
347            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
348            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
349            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
350            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
351            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
352            StreamingPriceField::Currency5 => "CURRENCY5",
353            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
354            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
355            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
356            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
357            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
358            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
359            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
360            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
361            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
362            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
363            StreamingPriceField::Timestamp => "TIMESTAMP",
364            StreamingPriceField::DlgFlag => "DLG_FLAG",
365        };
366        write!(f, "{}", field_name)
367    }
368}
369
370impl Display for StreamingPriceField {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        write!(f, "{:?}", self)
373    }
374}
375
376/// Constructs a vector of serialized streaming price field names from a given set of `StreamingPriceField`.
377///
378/// # Arguments
379///
380/// * `fields` - A reference to a `HashSet` containing `StreamingPriceField` items that need to be serialized.
381///
382/// # Returns
383///
384/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingPriceField` from the input set.
385///
386/// # Panics
387///
388/// This function will panic if the serialization of any `StreamingPriceField` fails.
389///
390pub(crate) fn get_streaming_price_fields(fields: &HashSet<StreamingPriceField>) -> Vec<String> {
391    // Map each enum variant to the exact IG Lightstreamer field identifier.
392    let map_field = |f: &StreamingPriceField| -> &'static str {
393        match f {
394            // Core prices
395            StreamingPriceField::MidOpen => "MID_OPEN",
396            StreamingPriceField::High => "HIGH",
397            StreamingPriceField::Low => "LOW",
398            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
399            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
400
401            // Bid ladder prices
402            StreamingPriceField::BidPrice1 => "BIDPRICE1",
403            StreamingPriceField::BidPrice2 => "BIDPRICE2",
404            StreamingPriceField::BidPrice3 => "BIDPRICE3",
405            StreamingPriceField::BidPrice4 => "BIDPRICE4",
406            StreamingPriceField::BidPrice5 => "BIDPRICE5",
407
408            // Ask ladder prices
409            StreamingPriceField::AskPrice1 => "ASKPRICE1",
410            StreamingPriceField::AskPrice2 => "ASKPRICE2",
411            StreamingPriceField::AskPrice3 => "ASKPRICE3",
412            StreamingPriceField::AskPrice4 => "ASKPRICE4",
413            StreamingPriceField::AskPrice5 => "ASKPRICE5",
414
415            // Bid sizes
416            StreamingPriceField::BidSize1 => "BIDSIZE1",
417            StreamingPriceField::BidSize2 => "BIDSIZE2",
418            StreamingPriceField::BidSize3 => "BIDSIZE3",
419            StreamingPriceField::BidSize4 => "BIDSIZE4",
420            StreamingPriceField::BidSize5 => "BIDSIZE5",
421
422            // Ask sizes
423            StreamingPriceField::AskSize1 => "ASKSIZE1",
424            StreamingPriceField::AskSize2 => "ASKSIZE2",
425            StreamingPriceField::AskSize3 => "ASKSIZE3",
426            StreamingPriceField::AskSize4 => "ASKSIZE4",
427            StreamingPriceField::AskSize5 => "ASKSIZE5",
428
429            // Currencies
430            StreamingPriceField::Currency0 => "CURRENCY0",
431            StreamingPriceField::Currency1 => "CURRENCY1",
432            StreamingPriceField::Currency2 => "CURRENCY2",
433            StreamingPriceField::Currency3 => "CURRENCY3",
434            StreamingPriceField::Currency4 => "CURRENCY4",
435            StreamingPriceField::Currency5 => "CURRENCY5",
436
437            // Currency 1 bid sizes
438            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
439            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
440            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
441            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
442            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
443            // Currency 1 ask sizes
444            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
445            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
446            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
447            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
448            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
449
450            // Currency 2 bid sizes
451            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
452            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
453            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
454            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
455            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
456            // Currency 2 ask sizes
457            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
458            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
459            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
460            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
461            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
462
463            // Currency 3 bid sizes
464            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
465            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
466            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
467            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
468            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
469            // Currency 3 ask sizes
470            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
471            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
472            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
473            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
474            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
475
476            // Currency 4 bid sizes
477            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
478            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
479            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
480            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
481            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
482            // Currency 4 ask sizes
483            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
484            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
485            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
486            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
487            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
488
489            // Currency 5 bid sizes
490            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
491            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
492            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
493            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
494            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
495            // Currency 5 ask sizes
496            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
497            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
498            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
499            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
500            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
501
502            // Misc
503            StreamingPriceField::Timestamp => "TIMESTAMP",
504            StreamingPriceField::DlgFlag => "DLG_FLAG",
505        }
506    };
507
508    let mut fields_vec = Vec::with_capacity(fields.len());
509    for field in fields {
510        fields_vec.push(map_field(field).to_string());
511    }
512    fields_vec
513}
514
515/// Streaming account data fields available for account subscriptions.
516///
517/// These fields represent the various account data points that can be subscribed to
518/// in the IG Markets streaming API for account updates.
519#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
520#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
521pub enum StreamingAccountDataField {
522    /// Profit and loss
523    #[default]
524    Pnl,
525    /// Deposit amount
526    Deposit,
527    /// Available cash
528    AvailableCash,
529    /// Profit and loss for long positions with guaranteed stops
530    PnlLr,
531    /// Profit and loss for long positions without guaranteed stops
532    PnlNlr,
533    /// Total funds
534    Funds,
535    /// Total margin
536    Margin,
537    /// Margin for positions with guaranteed stops
538    MarginLr,
539    /// Margin for positions without guaranteed stops
540    MarginNlr,
541    /// Available amount to deal
542    AvailableToDeal,
543    /// Total equity
544    Equity,
545    /// Equity used
546    EquityUsed,
547}
548
549impl Debug for StreamingAccountDataField {
550    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551        let field_name = match self {
552            StreamingAccountDataField::Pnl => "PNL",
553            StreamingAccountDataField::Deposit => "DEPOSIT",
554            StreamingAccountDataField::AvailableCash => "AVAILABLE_CASH",
555            StreamingAccountDataField::PnlLr => "PNL_LR",
556            StreamingAccountDataField::PnlNlr => "PNL_NLR",
557            StreamingAccountDataField::Funds => "FUNDS",
558            StreamingAccountDataField::Margin => "MARGIN",
559            StreamingAccountDataField::MarginLr => "MARGIN_LR",
560            StreamingAccountDataField::MarginNlr => "MARGIN_NLR",
561            StreamingAccountDataField::AvailableToDeal => "AVAILABLE_TO_DEAL",
562            StreamingAccountDataField::Equity => "EQUITY",
563            StreamingAccountDataField::EquityUsed => "EQUITY_USED",
564        };
565        write!(f, "{}", field_name)
566    }
567}
568
569impl Display for StreamingAccountDataField {
570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571        write!(f, "{:?}", self)
572    }
573}
574
575/// Constructs a vector of serialized streaming account data field names from a given set of `StreamingAccountDataField`.
576///
577/// # Arguments
578///
579/// * `fields` - A reference to a `HashSet` containing `StreamingAccountDataField` items that need to be serialized.
580///
581/// # Returns
582///
583/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingAccountDataField` from the input set.
584///
585/// # Panics
586///
587/// This function will panic if the serialization of any `StreamingAccountDataField` fails.
588///
589pub(crate) fn get_streaming_account_data_fields(
590    fields: &HashSet<StreamingAccountDataField>,
591) -> Vec<String> {
592    let mut fields_vec = Vec::new();
593    for field in fields {
594        let val =
595            serde_json::to_value(field).expect("Failed to serialize StreamingAccountDataField");
596        match val {
597            serde_json::Value::String(s) => fields_vec.push(s),
598            _ => fields_vec.push(format!("{:?}", field)),
599        }
600    }
601    fields_vec
602}
603
604/// Streaming chart fields available for chart subscriptions (tick and candle).
605///
606/// These fields represent both tick-level and aggregated (candle) chart data
607/// provided by the IG Markets Lightstreamer streaming API.
608#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
609#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
610pub enum StreamingChartField {
611    // Common fields (available in both tick and candle data)
612    /// Last traded volume for the period (tick or candle)
613    #[default]
614    Ltv,
615    /// Incremental trading volume since last update
616    Ttv,
617    /// Update time as milliseconds from the Epoch
618    Utm,
619    /// Mid-market price at the start of the day
620    DayOpenMid,
621    /// Change from day's opening mid price to current mid price
622    DayNetChgMid,
623    /// Daily percentage change in mid price
624    DayPercChgMid,
625    /// Highest mid price for the day
626    DayHigh,
627    /// Lowest mid price for the day
628    DayLow,
629
630    // Tick-only fields (DISTINCT mode, CHART:{epic}:TICK)
631    /// Current bid price
632    Bid,
633    /// Current offer/ask price
634    Ofr,
635    /// Last traded price
636    Ltp,
637
638    // Candle-only fields (MERGE mode, CHART:{epic}:{scale})
639    /// Candle open price (offer)
640    OfrOpen,
641    /// Candle high price (offer)
642    OfrHigh,
643    /// Candle low price (offer)
644    OfrLow,
645    /// Candle closing price (offer)
646    OfrClose,
647    /// Candle open price (bid)
648    BidOpen,
649    /// Candle high price (bid)
650    BidHigh,
651    /// Candle low price (bid)
652    BidLow,
653    /// Candle closing price (bid)
654    BidClose,
655    /// Candle open price (last traded price)
656    LtpOpen,
657    /// Candle high price (last traded price)
658    LtpHigh,
659    /// Candle low price (last traded price)
660    LtpLow,
661    /// Candle closing price (last traded price)
662    LtpClose,
663    /// Indicator that candle ended (1 when candle ends, 0 otherwise)
664    ConsEnd,
665    /// Number of ticks consolidated in the candle
666    ConsTickCount,
667}
668
669impl std::fmt::Debug for StreamingChartField {
670    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671        let name = match self {
672            StreamingChartField::Ltv => "LTV",
673            StreamingChartField::Ttv => "TTV",
674            StreamingChartField::Utm => "UTM",
675            StreamingChartField::DayOpenMid => "DAY_OPEN_MID",
676            StreamingChartField::DayNetChgMid => "DAY_NET_CHG_MID",
677            StreamingChartField::DayPercChgMid => "DAY_PERC_CHG_MID",
678            StreamingChartField::DayHigh => "DAY_HIGH",
679            StreamingChartField::DayLow => "DAY_LOW",
680
681            StreamingChartField::Bid => "BID",
682            StreamingChartField::Ofr => "OFR",
683            StreamingChartField::Ltp => "LTP",
684
685            StreamingChartField::OfrOpen => "OFR_OPEN",
686            StreamingChartField::OfrHigh => "OFR_HIGH",
687            StreamingChartField::OfrLow => "OFR_LOW",
688            StreamingChartField::OfrClose => "OFR_CLOSE",
689            StreamingChartField::BidOpen => "BID_OPEN",
690            StreamingChartField::BidHigh => "BID_HIGH",
691            StreamingChartField::BidLow => "BID_LOW",
692            StreamingChartField::BidClose => "BID_CLOSE",
693            StreamingChartField::LtpOpen => "LTP_OPEN",
694            StreamingChartField::LtpHigh => "LTP_HIGH",
695            StreamingChartField::LtpLow => "LTP_LOW",
696            StreamingChartField::LtpClose => "LTP_CLOSE",
697            StreamingChartField::ConsEnd => "CONS_END",
698            StreamingChartField::ConsTickCount => "CONS_TICK_COUNT",
699        };
700        write!(f, "{}", name)
701    }
702}
703
704impl std::fmt::Display for StreamingChartField {
705    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
706        write!(f, "{:?}", self)
707    }
708}
709
710/// Constructs a vector of serialized streaming chart field names from a given set of `StreamingChartField`.
711///
712/// # Arguments
713///
714/// * `fields` - A reference to a `HashSet` containing `StreamingChartField` items.
715///
716/// # Returns
717///
718/// A `Vec<String>` of serialized field names for Lightstreamer subscriptions.
719///
720/// # Panics
721///
722/// Panics if serialization of any field fails.
723pub(crate) fn get_streaming_chart_fields(fields: &HashSet<StreamingChartField>) -> Vec<String> {
724    let mut out = Vec::with_capacity(fields.len());
725    for field in fields {
726        let val = serde_json::to_value(field).expect("Failed to serialize StreamingChartField");
727        match val {
728            serde_json::Value::String(s) => out.push(s),
729            _ => out.push(format!("{:?}", field)),
730        }
731    }
732    out
733}