Skip to main content

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    /// Price change versus open
278    #[serde(rename = "NET_CHG")]
279    NetChg,
280    /// Percentage change versus open
281    #[serde(rename = "NET_CHG_PCT")]
282    NetChgPct,
283    /// Delayed price flag (0 = false, 1 = true)
284    Delay,
285}
286
287impl Debug for StreamingPriceField {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        let field_name = match self {
290            StreamingPriceField::MidOpen => "MID_OPEN",
291            StreamingPriceField::High => "HIGH",
292            StreamingPriceField::Low => "LOW",
293            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
294            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
295            StreamingPriceField::BidPrice1 => "BIDPRICE1",
296            StreamingPriceField::BidPrice2 => "BIDPRICE2",
297            StreamingPriceField::BidPrice3 => "BIDPRICE3",
298            StreamingPriceField::BidPrice4 => "BIDPRICE4",
299            StreamingPriceField::BidPrice5 => "BIDPRICE5",
300            StreamingPriceField::AskPrice1 => "ASKPRICE1",
301            StreamingPriceField::AskPrice2 => "ASKPRICE2",
302            StreamingPriceField::AskPrice3 => "ASKPRICE3",
303            StreamingPriceField::AskPrice4 => "ASKPRICE4",
304            StreamingPriceField::AskPrice5 => "ASKPRICE5",
305            StreamingPriceField::BidSize1 => "BIDSIZE1",
306            StreamingPriceField::BidSize2 => "BIDSIZE2",
307            StreamingPriceField::BidSize3 => "BIDSIZE3",
308            StreamingPriceField::BidSize4 => "BIDSIZE4",
309            StreamingPriceField::BidSize5 => "BIDSIZE5",
310            StreamingPriceField::AskSize1 => "ASKSIZE1",
311            StreamingPriceField::AskSize2 => "ASKSIZE2",
312            StreamingPriceField::AskSize3 => "ASKSIZE3",
313            StreamingPriceField::AskSize4 => "ASKSIZE4",
314            StreamingPriceField::AskSize5 => "ASKSIZE5",
315            StreamingPriceField::Currency0 => "CURRENCY0",
316            StreamingPriceField::Currency1 => "CURRENCY1",
317            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
318            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
319            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
320            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
321            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
322            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
323            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
324            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
325            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
326            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
327            StreamingPriceField::Currency2 => "CURRENCY2",
328            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
329            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
330            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
331            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
332            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
333            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
334            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
335            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
336            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
337            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
338            StreamingPriceField::Currency3 => "CURRENCY3",
339            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
340            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
341            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
342            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
343            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
344            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
345            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
346            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
347            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
348            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
349            StreamingPriceField::Currency4 => "CURRENCY4",
350            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
351            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
352            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
353            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
354            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
355            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
356            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
357            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
358            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
359            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
360            StreamingPriceField::Currency5 => "CURRENCY5",
361            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
362            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
363            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
364            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
365            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
366            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
367            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
368            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
369            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
370            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
371            StreamingPriceField::Timestamp => "TIMESTAMP",
372            StreamingPriceField::DlgFlag => "DLG_FLAG",
373            StreamingPriceField::NetChg => "NET_CHG",
374            StreamingPriceField::NetChgPct => "NET_CHG_PCT",
375            StreamingPriceField::Delay => "DELAY",
376        };
377        write!(f, "{}", field_name)
378    }
379}
380
381impl Display for StreamingPriceField {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        write!(f, "{:?}", self)
384    }
385}
386
387/// Constructs a vector of serialized streaming price field names from a given set of `StreamingPriceField`.
388///
389/// # Arguments
390///
391/// * `fields` - A reference to a `HashSet` containing `StreamingPriceField` items that need to be serialized.
392///
393/// # Returns
394///
395/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingPriceField` from the input set.
396///
397/// # Panics
398///
399/// This function will panic if the serialization of any `StreamingPriceField` fails.
400///
401pub(crate) fn get_streaming_price_fields(fields: &HashSet<StreamingPriceField>) -> Vec<String> {
402    // Map each enum variant to the exact IG Lightstreamer field identifier.
403    let map_field = |f: &StreamingPriceField| -> &'static str {
404        match f {
405            // Core prices
406            StreamingPriceField::MidOpen => "MID_OPEN",
407            StreamingPriceField::High => "HIGH",
408            StreamingPriceField::Low => "LOW",
409            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
410            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
411
412            // Bid ladder prices
413            StreamingPriceField::BidPrice1 => "BIDPRICE1",
414            StreamingPriceField::BidPrice2 => "BIDPRICE2",
415            StreamingPriceField::BidPrice3 => "BIDPRICE3",
416            StreamingPriceField::BidPrice4 => "BIDPRICE4",
417            StreamingPriceField::BidPrice5 => "BIDPRICE5",
418
419            // Ask ladder prices
420            StreamingPriceField::AskPrice1 => "ASKPRICE1",
421            StreamingPriceField::AskPrice2 => "ASKPRICE2",
422            StreamingPriceField::AskPrice3 => "ASKPRICE3",
423            StreamingPriceField::AskPrice4 => "ASKPRICE4",
424            StreamingPriceField::AskPrice5 => "ASKPRICE5",
425
426            // Bid sizes
427            StreamingPriceField::BidSize1 => "BIDSIZE1",
428            StreamingPriceField::BidSize2 => "BIDSIZE2",
429            StreamingPriceField::BidSize3 => "BIDSIZE3",
430            StreamingPriceField::BidSize4 => "BIDSIZE4",
431            StreamingPriceField::BidSize5 => "BIDSIZE5",
432
433            // Ask sizes
434            StreamingPriceField::AskSize1 => "ASKSIZE1",
435            StreamingPriceField::AskSize2 => "ASKSIZE2",
436            StreamingPriceField::AskSize3 => "ASKSIZE3",
437            StreamingPriceField::AskSize4 => "ASKSIZE4",
438            StreamingPriceField::AskSize5 => "ASKSIZE5",
439
440            // Currencies
441            StreamingPriceField::Currency0 => "CURRENCY0",
442            StreamingPriceField::Currency1 => "CURRENCY1",
443            StreamingPriceField::Currency2 => "CURRENCY2",
444            StreamingPriceField::Currency3 => "CURRENCY3",
445            StreamingPriceField::Currency4 => "CURRENCY4",
446            StreamingPriceField::Currency5 => "CURRENCY5",
447
448            // Currency 1 bid sizes
449            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
450            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
451            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
452            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
453            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
454            // Currency 1 ask sizes
455            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
456            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
457            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
458            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
459            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
460
461            // Currency 2 bid sizes
462            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
463            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
464            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
465            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
466            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
467            // Currency 2 ask sizes
468            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
469            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
470            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
471            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
472            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
473
474            // Currency 3 bid sizes
475            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
476            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
477            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
478            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
479            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
480            // Currency 3 ask sizes
481            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
482            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
483            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
484            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
485            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
486
487            // Currency 4 bid sizes
488            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
489            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
490            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
491            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
492            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
493            // Currency 4 ask sizes
494            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
495            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
496            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
497            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
498            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
499
500            // Currency 5 bid sizes
501            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
502            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
503            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
504            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
505            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
506            // Currency 5 ask sizes
507            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
508            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
509            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
510            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
511            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
512
513            // Misc
514            StreamingPriceField::Timestamp => "TIMESTAMP",
515            StreamingPriceField::DlgFlag => "DLG_FLAG",
516            StreamingPriceField::NetChg => "NET_CHG",
517            StreamingPriceField::NetChgPct => "NET_CHG_PCT",
518            StreamingPriceField::Delay => "DELAY",
519        }
520    };
521
522    let mut fields_vec = Vec::with_capacity(fields.len());
523    for field in fields {
524        fields_vec.push(map_field(field).to_string());
525    }
526    fields_vec
527}
528
529/// Streaming account data fields available for account subscriptions.
530///
531/// These fields represent the various account data points that can be subscribed to
532/// in the IG Markets streaming API for account updates.
533#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
534#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
535pub enum StreamingAccountDataField {
536    /// Profit and loss
537    #[default]
538    Pnl,
539    /// Deposit amount
540    Deposit,
541    /// Available cash
542    AvailableCash,
543    /// Profit and loss for long positions with guaranteed stops
544    PnlLr,
545    /// Profit and loss for long positions without guaranteed stops
546    PnlNlr,
547    /// Total funds
548    Funds,
549    /// Total margin
550    Margin,
551    /// Margin for positions with guaranteed stops
552    MarginLr,
553    /// Margin for positions without guaranteed stops
554    MarginNlr,
555    /// Available amount to deal
556    AvailableToDeal,
557    /// Total equity
558    Equity,
559    /// Equity used
560    EquityUsed,
561}
562
563impl Debug for StreamingAccountDataField {
564    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565        let field_name = match self {
566            StreamingAccountDataField::Pnl => "PNL",
567            StreamingAccountDataField::Deposit => "DEPOSIT",
568            StreamingAccountDataField::AvailableCash => "AVAILABLE_CASH",
569            StreamingAccountDataField::PnlLr => "PNL_LR",
570            StreamingAccountDataField::PnlNlr => "PNL_NLR",
571            StreamingAccountDataField::Funds => "FUNDS",
572            StreamingAccountDataField::Margin => "MARGIN",
573            StreamingAccountDataField::MarginLr => "MARGIN_LR",
574            StreamingAccountDataField::MarginNlr => "MARGIN_NLR",
575            StreamingAccountDataField::AvailableToDeal => "AVAILABLE_TO_DEAL",
576            StreamingAccountDataField::Equity => "EQUITY",
577            StreamingAccountDataField::EquityUsed => "EQUITY_USED",
578        };
579        write!(f, "{}", field_name)
580    }
581}
582
583impl Display for StreamingAccountDataField {
584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585        write!(f, "{:?}", self)
586    }
587}
588
589/// Constructs a vector of serialized streaming account data field names from a given set of `StreamingAccountDataField`.
590///
591/// # Arguments
592///
593/// * `fields` - A reference to a `HashSet` containing `StreamingAccountDataField` items that need to be serialized.
594///
595/// # Returns
596///
597/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingAccountDataField` from the input set.
598///
599/// # Panics
600///
601/// This function will panic if the serialization of any `StreamingAccountDataField` fails.
602///
603pub(crate) fn get_streaming_account_data_fields(
604    fields: &HashSet<StreamingAccountDataField>,
605) -> Vec<String> {
606    let mut fields_vec = Vec::new();
607    for field in fields {
608        let val =
609            serde_json::to_value(field).expect("Failed to serialize StreamingAccountDataField");
610        match val {
611            serde_json::Value::String(s) => fields_vec.push(s),
612            _ => fields_vec.push(format!("{:?}", field)),
613        }
614    }
615    fields_vec
616}
617
618/// Streaming chart fields available for chart subscriptions (tick and candle).
619///
620/// These fields represent both tick-level and aggregated (candle) chart data
621/// provided by the IG Markets Lightstreamer streaming API.
622#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
623#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
624pub enum StreamingChartField {
625    // Common fields (available in both tick and candle data)
626    /// Last traded volume for the period (tick or candle)
627    #[default]
628    Ltv,
629    /// Incremental trading volume since last update
630    Ttv,
631    /// Update time as milliseconds from the Epoch
632    Utm,
633    /// Mid-market price at the start of the day
634    DayOpenMid,
635    /// Change from day's opening mid price to current mid price
636    DayNetChgMid,
637    /// Daily percentage change in mid price
638    DayPercChgMid,
639    /// Highest mid price for the day
640    DayHigh,
641    /// Lowest mid price for the day
642    DayLow,
643
644    // Tick-only fields (DISTINCT mode, CHART:{epic}:TICK)
645    /// Current bid price
646    Bid,
647    /// Current offer/ask price
648    Ofr,
649    /// Last traded price
650    Ltp,
651
652    // Candle-only fields (MERGE mode, CHART:{epic}:{scale})
653    /// Candle open price (offer)
654    OfrOpen,
655    /// Candle high price (offer)
656    OfrHigh,
657    /// Candle low price (offer)
658    OfrLow,
659    /// Candle closing price (offer)
660    OfrClose,
661    /// Candle open price (bid)
662    BidOpen,
663    /// Candle high price (bid)
664    BidHigh,
665    /// Candle low price (bid)
666    BidLow,
667    /// Candle closing price (bid)
668    BidClose,
669    /// Candle open price (last traded price)
670    LtpOpen,
671    /// Candle high price (last traded price)
672    LtpHigh,
673    /// Candle low price (last traded price)
674    LtpLow,
675    /// Candle closing price (last traded price)
676    LtpClose,
677    /// Indicator that candle ended (1 when candle ends, 0 otherwise)
678    ConsEnd,
679    /// Number of ticks consolidated in the candle
680    ConsTickCount,
681}
682
683impl std::fmt::Debug for StreamingChartField {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        let name = match self {
686            StreamingChartField::Ltv => "LTV",
687            StreamingChartField::Ttv => "TTV",
688            StreamingChartField::Utm => "UTM",
689            StreamingChartField::DayOpenMid => "DAY_OPEN_MID",
690            StreamingChartField::DayNetChgMid => "DAY_NET_CHG_MID",
691            StreamingChartField::DayPercChgMid => "DAY_PERC_CHG_MID",
692            StreamingChartField::DayHigh => "DAY_HIGH",
693            StreamingChartField::DayLow => "DAY_LOW",
694
695            StreamingChartField::Bid => "BID",
696            StreamingChartField::Ofr => "OFR",
697            StreamingChartField::Ltp => "LTP",
698
699            StreamingChartField::OfrOpen => "OFR_OPEN",
700            StreamingChartField::OfrHigh => "OFR_HIGH",
701            StreamingChartField::OfrLow => "OFR_LOW",
702            StreamingChartField::OfrClose => "OFR_CLOSE",
703            StreamingChartField::BidOpen => "BID_OPEN",
704            StreamingChartField::BidHigh => "BID_HIGH",
705            StreamingChartField::BidLow => "BID_LOW",
706            StreamingChartField::BidClose => "BID_CLOSE",
707            StreamingChartField::LtpOpen => "LTP_OPEN",
708            StreamingChartField::LtpHigh => "LTP_HIGH",
709            StreamingChartField::LtpLow => "LTP_LOW",
710            StreamingChartField::LtpClose => "LTP_CLOSE",
711            StreamingChartField::ConsEnd => "CONS_END",
712            StreamingChartField::ConsTickCount => "CONS_TICK_COUNT",
713        };
714        write!(f, "{}", name)
715    }
716}
717
718impl std::fmt::Display for StreamingChartField {
719    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720        write!(f, "{:?}", self)
721    }
722}
723
724/// Constructs a vector of serialized streaming chart field names from a given set of `StreamingChartField`.
725///
726/// # Arguments
727///
728/// * `fields` - A reference to a `HashSet` containing `StreamingChartField` items.
729///
730/// # Returns
731///
732/// A `Vec<String>` of serialized field names for Lightstreamer subscriptions.
733///
734/// # Panics
735///
736/// Panics if serialization of any field fails.
737pub(crate) fn get_streaming_chart_fields(fields: &HashSet<StreamingChartField>) -> Vec<String> {
738    let mut out = Vec::with_capacity(fields.len());
739    for field in fields {
740        let val = serde_json::to_value(field).expect("Failed to serialize StreamingChartField");
741        match val {
742            serde_json::Value::String(s) => out.push(s),
743            _ => out.push(format!("{:?}", field)),
744        }
745    }
746    out
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    #[test]
754    fn test_streaming_market_field_default() {
755        let field = StreamingMarketField::default();
756        assert_eq!(field, StreamingMarketField::Offer);
757    }
758
759    #[test]
760    fn test_streaming_market_field_debug() {
761        assert_eq!(format!("{:?}", StreamingMarketField::Bid), "BID");
762        assert_eq!(format!("{:?}", StreamingMarketField::Offer), "OFFER");
763        assert_eq!(format!("{:?}", StreamingMarketField::High), "HIGH");
764        assert_eq!(format!("{:?}", StreamingMarketField::Low), "LOW");
765        assert_eq!(format!("{:?}", StreamingMarketField::Change), "CHANGE");
766        assert_eq!(
767            format!("{:?}", StreamingMarketField::ChangePct),
768            "CHANGE_PCT"
769        );
770        assert_eq!(
771            format!("{:?}", StreamingMarketField::UpdateTime),
772            "UPDATE_TIME"
773        );
774        assert_eq!(
775            format!("{:?}", StreamingMarketField::MarketDelay),
776            "MARKET_DELAY"
777        );
778        assert_eq!(
779            format!("{:?}", StreamingMarketField::MarketState),
780            "MARKET_STATE"
781        );
782        assert_eq!(format!("{:?}", StreamingMarketField::MidOpen), "MID_OPEN");
783    }
784
785    #[test]
786    fn test_streaming_market_field_display() {
787        assert_eq!(format!("{}", StreamingMarketField::Bid), "BID");
788        assert_eq!(format!("{}", StreamingMarketField::Offer), "OFFER");
789    }
790
791    #[test]
792    fn test_get_streaming_market_fields_empty() {
793        let fields: HashSet<StreamingMarketField> = HashSet::new();
794        let result = get_streaming_market_fields(&fields);
795        assert!(result.is_empty());
796    }
797
798    #[test]
799    fn test_get_streaming_market_fields_single() {
800        let mut fields = HashSet::new();
801        fields.insert(StreamingMarketField::Bid);
802        let result = get_streaming_market_fields(&fields);
803        assert_eq!(result.len(), 1);
804        assert!(result.contains(&"BID".to_string()));
805    }
806
807    #[test]
808    fn test_get_streaming_market_fields_multiple() {
809        let mut fields = HashSet::new();
810        fields.insert(StreamingMarketField::Bid);
811        fields.insert(StreamingMarketField::Offer);
812        fields.insert(StreamingMarketField::High);
813        let result = get_streaming_market_fields(&fields);
814        assert_eq!(result.len(), 3);
815        assert!(result.contains(&"BID".to_string()));
816        assert!(result.contains(&"OFFER".to_string()));
817        assert!(result.contains(&"HIGH".to_string()));
818    }
819
820    #[test]
821    fn test_streaming_price_field_default() {
822        let field = StreamingPriceField::default();
823        assert_eq!(field, StreamingPriceField::AskPrice5);
824    }
825
826    #[test]
827    fn test_streaming_price_field_debug() {
828        assert_eq!(format!("{:?}", StreamingPriceField::High), "HIGH");
829        assert_eq!(format!("{:?}", StreamingPriceField::Low), "LOW");
830        assert_eq!(format!("{:?}", StreamingPriceField::MidOpen), "MID_OPEN");
831        assert_eq!(format!("{:?}", StreamingPriceField::BidPrice1), "BIDPRICE1");
832        assert_eq!(format!("{:?}", StreamingPriceField::AskPrice1), "ASKPRICE1");
833    }
834
835    #[test]
836    fn test_streaming_price_field_display() {
837        assert_eq!(format!("{}", StreamingPriceField::High), "HIGH");
838        assert_eq!(format!("{}", StreamingPriceField::BidPrice1), "BIDPRICE1");
839    }
840
841    #[test]
842    fn test_streaming_account_field_default() {
843        let field = StreamingAccountDataField::default();
844        assert_eq!(field, StreamingAccountDataField::Pnl);
845    }
846
847    #[test]
848    fn test_streaming_account_field_debug() {
849        assert_eq!(format!("{:?}", StreamingAccountDataField::Pnl), "PNL");
850        assert_eq!(
851            format!("{:?}", StreamingAccountDataField::Deposit),
852            "DEPOSIT"
853        );
854        assert_eq!(format!("{:?}", StreamingAccountDataField::Margin), "MARGIN");
855        assert_eq!(format!("{:?}", StreamingAccountDataField::Equity), "EQUITY");
856    }
857
858    #[test]
859    fn test_streaming_account_field_display() {
860        assert_eq!(format!("{}", StreamingAccountDataField::Pnl), "PNL");
861        assert_eq!(format!("{}", StreamingAccountDataField::Equity), "EQUITY");
862    }
863
864    #[test]
865    fn test_get_streaming_account_fields_empty() {
866        let fields: HashSet<StreamingAccountDataField> = HashSet::new();
867        let result = get_streaming_account_data_fields(&fields);
868        assert!(result.is_empty());
869    }
870
871    #[test]
872    fn test_get_streaming_account_fields_multiple() {
873        let mut fields = HashSet::new();
874        fields.insert(StreamingAccountDataField::Pnl);
875        fields.insert(StreamingAccountDataField::Equity);
876        let result = get_streaming_account_data_fields(&fields);
877        assert_eq!(result.len(), 2);
878        assert!(result.contains(&"PNL".to_string()));
879        assert!(result.contains(&"EQUITY".to_string()));
880    }
881
882    #[test]
883    fn test_streaming_chart_field_default() {
884        let field = StreamingChartField::default();
885        assert_eq!(field, StreamingChartField::Ltv);
886    }
887
888    #[test]
889    fn test_streaming_chart_field_debug() {
890        assert_eq!(format!("{:?}", StreamingChartField::Bid), "BID");
891        assert_eq!(format!("{:?}", StreamingChartField::Ofr), "OFR");
892        assert_eq!(format!("{:?}", StreamingChartField::Ltp), "LTP");
893        assert_eq!(format!("{:?}", StreamingChartField::Ltv), "LTV");
894        assert_eq!(format!("{:?}", StreamingChartField::Utm), "UTM");
895        assert_eq!(format!("{:?}", StreamingChartField::DayHigh), "DAY_HIGH");
896        assert_eq!(format!("{:?}", StreamingChartField::DayLow), "DAY_LOW");
897    }
898
899    #[test]
900    fn test_streaming_chart_field_display() {
901        assert_eq!(format!("{}", StreamingChartField::Bid), "BID");
902        assert_eq!(format!("{}", StreamingChartField::Ofr), "OFR");
903    }
904
905    #[test]
906    fn test_get_streaming_chart_fields_empty() {
907        let fields: HashSet<StreamingChartField> = HashSet::new();
908        let result = get_streaming_chart_fields(&fields);
909        assert!(result.is_empty());
910    }
911
912    #[test]
913    fn test_get_streaming_chart_fields_multiple() {
914        let mut fields = HashSet::new();
915        fields.insert(StreamingChartField::Bid);
916        fields.insert(StreamingChartField::Ofr);
917        fields.insert(StreamingChartField::Ltp);
918        let result = get_streaming_chart_fields(&fields);
919        assert_eq!(result.len(), 3);
920        assert!(result.contains(&"BID".to_string()));
921        assert!(result.contains(&"OFR".to_string()));
922        assert!(result.contains(&"LTP".to_string()));
923    }
924
925    #[test]
926    fn test_streaming_market_field_serialization() {
927        let field = StreamingMarketField::Bid;
928        let json = serde_json::to_string(&field).expect("serialize failed");
929        assert_eq!(json, "\"BID\"");
930
931        let deserialized: StreamingMarketField =
932            serde_json::from_str(&json).expect("deserialize failed");
933        assert_eq!(deserialized, StreamingMarketField::Bid);
934    }
935
936    #[test]
937    fn test_streaming_account_field_serialization() {
938        let field = StreamingAccountDataField::Pnl;
939        let json = serde_json::to_string(&field).expect("serialize failed");
940        assert_eq!(json, "\"PNL\"");
941
942        let deserialized: StreamingAccountDataField =
943            serde_json::from_str(&json).expect("deserialize failed");
944        assert_eq!(deserialized, StreamingAccountDataField::Pnl);
945    }
946
947    #[test]
948    fn test_streaming_chart_field_serialization() {
949        let field = StreamingChartField::Bid;
950        let json = serde_json::to_string(&field).expect("serialize failed");
951        assert_eq!(json, "\"BID\"");
952
953        let deserialized: StreamingChartField =
954            serde_json::from_str(&json).expect("deserialize failed");
955        assert_eq!(deserialized, StreamingChartField::Bid);
956    }
957
958    #[test]
959    fn test_streaming_market_field_equality() {
960        let field1 = StreamingMarketField::Bid;
961        let field2 = StreamingMarketField::Bid;
962        let field3 = StreamingMarketField::Offer;
963
964        assert_eq!(field1, field2);
965        assert_ne!(field1, field3);
966    }
967
968    #[test]
969    fn test_streaming_market_field_hash() {
970        let mut set = HashSet::new();
971        set.insert(StreamingMarketField::Bid);
972        set.insert(StreamingMarketField::Bid); // Duplicate
973
974        assert_eq!(set.len(), 1);
975    }
976
977    #[test]
978    fn test_streaming_market_field_clone() {
979        let field = StreamingMarketField::High;
980        let cloned = field.clone();
981        assert_eq!(field, cloned);
982    }
983}