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 serde::{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 the exact IG Lightstreamer wire name
82/// of a `StreamingMarketField` from the input set.
83pub(crate) fn get_streaming_market_fields(fields: &HashSet<StreamingMarketField>) -> Vec<String> {
84    // `Display` (via the `Debug` impl above) emits the exact IG Lightstreamer
85    // wire field name for every variant, so no fallible serialization is needed.
86    fields.iter().map(|field| field.to_string()).collect()
87}
88
89/// Streaming price fields available for price subscriptions.
90///
91/// These fields represent the various price data points that can be subscribed to
92/// in the IG Markets streaming API for price updates.
93#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
94#[serde(rename_all = "UPPERCASE")]
95pub enum StreamingPriceField {
96    /// Mid open price
97    #[serde(rename = "MID_OPEN")]
98    MidOpen,
99    /// High price
100    High,
101    /// Low price
102    Low,
103    /// Bid quote ID
104    BidQuoteId,
105    /// Ask quote ID
106    AskQuoteId,
107    /// Bid price level 1
108    BidPrice1,
109    /// Bid price level 2
110    BidPrice2,
111    /// Bid price level 3
112    BidPrice3,
113    /// Bid price level 4
114    BidPrice4,
115    /// Bid price level 5
116    BidPrice5,
117    /// Ask price level 1
118    AskPrice1,
119    /// Ask price level 2
120    AskPrice2,
121    /// Ask price level 3
122    AskPrice3,
123    /// Ask price level 4
124    AskPrice4,
125    /// Ask price level 5
126    #[default]
127    AskPrice5,
128    /// Bid size level 1
129    BidSize1,
130    /// Bid size level 2
131    BidSize2,
132    /// Bid size level 3
133    BidSize3,
134    /// Bid size level 4
135    BidSize4,
136    /// Bid size level 5
137    BidSize5,
138    /// Ask size level 1
139    AskSize1,
140    /// Ask size level 2
141    AskSize2,
142    /// Ask size level 3
143    AskSize3,
144    /// Ask size level 4
145    AskSize4,
146    /// Ask size level 5
147    AskSize5,
148    /// Currency 0
149    Currency0,
150    /// Currency 1
151    Currency1,
152    /// Currency 1 bid size level 1
153    C1BidSize1,
154    /// Currency 1 bid size level 2
155    C1BidSize2,
156    /// Currency 1 bid size level 3
157    C1BidSize3,
158    /// Currency 1 bid size level 4
159    C1BidSize4,
160    /// Currency 1 bid size level 5
161    C1BidSize5,
162    /// Currency 1 ask size level 1
163    C1AskSize1,
164    /// Currency 1 ask size level 2
165    C1AskSize2,
166    /// Currency 1 ask size level 3
167    C1AskSize3,
168    /// Currency 1 ask size level 4
169    C1AskSize4,
170    /// Currency 1 ask size level 5
171    C1AskSize5,
172    /// Currency 2
173    Currency2,
174    /// Currency 2 bid size level 1
175    C2BidSize1,
176    /// Currency 2 bid size level 2
177    C2BidSize2,
178    /// Currency 2 bid size level 3
179    C2BidSize3,
180    /// Currency 2 bid size level 4
181    C2BidSize4,
182    /// Currency 2 bid size level 5
183    C2BidSize5,
184    /// Currency 2 ask size level 1
185    C2AskSize1,
186    /// Currency 2 ask size level 2
187    C2AskSize2,
188    /// Currency 2 ask size level 3
189    C2AskSize3,
190    /// Currency 2 ask size level 4
191    C2AskSize4,
192    /// Currency 2 ask size level 5
193    C2AskSize5,
194    /// Currency 3
195    Currency3,
196    /// Currency 3 bid size level 1
197    C3BidSize1,
198    /// Currency 3 bid size level 2
199    C3BidSize2,
200    /// Currency 3 bid size level 3
201    C3BidSize3,
202    /// Currency 3 bid size level 4
203    C3BidSize4,
204    /// Currency 3 bid size level 5
205    C3BidSize5,
206    /// Currency 3 ask size level 1
207    C3AskSize1,
208    /// Currency 3 ask size level 2
209    C3AskSize2,
210    /// Currency 3 ask size level 3
211    C3AskSize3,
212    /// Currency 3 ask size level 4
213    C3AskSize4,
214    /// Currency 3 ask size level 5
215    C3AskSize5,
216    /// Currency 4
217    Currency4,
218    /// Currency 4 bid size level 1
219    C4BidSize1,
220    /// Currency 4 bid size level 2
221    C4BidSize2,
222    /// Currency 4 bid size level 3
223    C4BidSize3,
224    /// Currency 4 bid size level 4
225    C4BidSize4,
226    /// Currency 4 bid size level 5
227    C4BidSize5,
228    /// Currency 4 ask size level 1
229    C4AskSize1,
230    /// Currency 4 ask size level 2
231    C4AskSize2,
232    /// Currency 4 ask size level 3
233    C4AskSize3,
234    /// Currency 4 ask size level 4
235    C4AskSize4,
236    /// Currency 4 ask size level 5
237    C4AskSize5,
238    /// Currency 5
239    Currency5,
240    /// Currency 5 bid size level 1
241    C5BidSize1,
242    /// Currency 5 bid size level 2
243    C5BidSize2,
244    /// Currency 5 bid size level 3
245    C5BidSize3,
246    /// Currency 5 bid size level 4
247    C5BidSize4,
248    /// Currency 5 bid size level 5
249    C5BidSize5,
250    /// Currency 5 ask size level 1
251    C5AskSize1,
252    /// Currency 5 ask size level 2
253    C5AskSize2,
254    /// Currency 5 ask size level 3
255    C5AskSize3,
256    /// Currency 5 ask size level 4
257    C5AskSize4,
258    /// Currency 5 ask size level 5
259    C5AskSize5,
260    /// Timestamp of the price update
261    Timestamp,
262    /// Dealing flag
263    #[serde(rename = "DLG_FLAG")]
264    DlgFlag,
265    /// Price change versus open
266    #[serde(rename = "NET_CHG")]
267    NetChg,
268    /// Percentage change versus open
269    #[serde(rename = "NET_CHG_PCT")]
270    NetChgPct,
271    /// Delayed price flag (0 = false, 1 = true)
272    Delay,
273}
274
275impl Debug for StreamingPriceField {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        let field_name = match self {
278            StreamingPriceField::MidOpen => "MID_OPEN",
279            StreamingPriceField::High => "HIGH",
280            StreamingPriceField::Low => "LOW",
281            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
282            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
283            StreamingPriceField::BidPrice1 => "BIDPRICE1",
284            StreamingPriceField::BidPrice2 => "BIDPRICE2",
285            StreamingPriceField::BidPrice3 => "BIDPRICE3",
286            StreamingPriceField::BidPrice4 => "BIDPRICE4",
287            StreamingPriceField::BidPrice5 => "BIDPRICE5",
288            StreamingPriceField::AskPrice1 => "ASKPRICE1",
289            StreamingPriceField::AskPrice2 => "ASKPRICE2",
290            StreamingPriceField::AskPrice3 => "ASKPRICE3",
291            StreamingPriceField::AskPrice4 => "ASKPRICE4",
292            StreamingPriceField::AskPrice5 => "ASKPRICE5",
293            StreamingPriceField::BidSize1 => "BIDSIZE1",
294            StreamingPriceField::BidSize2 => "BIDSIZE2",
295            StreamingPriceField::BidSize3 => "BIDSIZE3",
296            StreamingPriceField::BidSize4 => "BIDSIZE4",
297            StreamingPriceField::BidSize5 => "BIDSIZE5",
298            StreamingPriceField::AskSize1 => "ASKSIZE1",
299            StreamingPriceField::AskSize2 => "ASKSIZE2",
300            StreamingPriceField::AskSize3 => "ASKSIZE3",
301            StreamingPriceField::AskSize4 => "ASKSIZE4",
302            StreamingPriceField::AskSize5 => "ASKSIZE5",
303            StreamingPriceField::Currency0 => "CURRENCY0",
304            StreamingPriceField::Currency1 => "CURRENCY1",
305            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
306            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
307            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
308            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
309            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
310            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
311            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
312            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
313            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
314            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
315            StreamingPriceField::Currency2 => "CURRENCY2",
316            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
317            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
318            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
319            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
320            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
321            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
322            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
323            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
324            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
325            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
326            StreamingPriceField::Currency3 => "CURRENCY3",
327            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
328            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
329            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
330            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
331            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
332            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
333            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
334            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
335            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
336            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
337            StreamingPriceField::Currency4 => "CURRENCY4",
338            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
339            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
340            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
341            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
342            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
343            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
344            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
345            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
346            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
347            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
348            StreamingPriceField::Currency5 => "CURRENCY5",
349            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
350            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
351            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
352            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
353            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
354            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
355            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
356            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
357            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
358            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
359            StreamingPriceField::Timestamp => "TIMESTAMP",
360            StreamingPriceField::DlgFlag => "DLG_FLAG",
361            StreamingPriceField::NetChg => "NET_CHG",
362            StreamingPriceField::NetChgPct => "NET_CHG_PCT",
363            StreamingPriceField::Delay => "DELAY",
364        };
365        write!(f, "{}", field_name)
366    }
367}
368
369impl Display for StreamingPriceField {
370    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371        write!(f, "{:?}", self)
372    }
373}
374
375/// Constructs a vector of serialized streaming price field names from a given set of `StreamingPriceField`.
376///
377/// # Arguments
378///
379/// * `fields` - A reference to a `HashSet` containing `StreamingPriceField` items that need to be serialized.
380///
381/// # Returns
382///
383/// A `Vec<String>` where each `String` is the exact IG Lightstreamer wire name
384/// of a `StreamingPriceField` from the input set.
385pub(crate) fn get_streaming_price_fields(fields: &HashSet<StreamingPriceField>) -> Vec<String> {
386    // `Display` (via the `Debug` impl above) is the single source of truth for
387    // the exact IG Lightstreamer wire field name of every variant, so no
388    // separate mapping table or fallible serialization is needed.
389    fields.iter().map(|field| field.to_string()).collect()
390}
391
392/// Streaming account data fields available for account subscriptions.
393///
394/// These fields represent the various account data points that can be subscribed to
395/// in the IG Markets streaming API for account updates.
396#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
397#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
398pub enum StreamingAccountDataField {
399    /// Profit and loss
400    #[default]
401    Pnl,
402    /// Deposit amount
403    Deposit,
404    /// Available cash
405    AvailableCash,
406    /// Profit and loss for long positions with guaranteed stops
407    PnlLr,
408    /// Profit and loss for long positions without guaranteed stops
409    PnlNlr,
410    /// Total funds
411    Funds,
412    /// Total margin
413    Margin,
414    /// Margin for positions with guaranteed stops
415    MarginLr,
416    /// Margin for positions without guaranteed stops
417    MarginNlr,
418    /// Available amount to deal
419    AvailableToDeal,
420    /// Total equity
421    Equity,
422    /// Equity used
423    EquityUsed,
424}
425
426impl Debug for StreamingAccountDataField {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        let field_name = match self {
429            StreamingAccountDataField::Pnl => "PNL",
430            StreamingAccountDataField::Deposit => "DEPOSIT",
431            StreamingAccountDataField::AvailableCash => "AVAILABLE_CASH",
432            StreamingAccountDataField::PnlLr => "PNL_LR",
433            StreamingAccountDataField::PnlNlr => "PNL_NLR",
434            StreamingAccountDataField::Funds => "FUNDS",
435            StreamingAccountDataField::Margin => "MARGIN",
436            StreamingAccountDataField::MarginLr => "MARGIN_LR",
437            StreamingAccountDataField::MarginNlr => "MARGIN_NLR",
438            StreamingAccountDataField::AvailableToDeal => "AVAILABLE_TO_DEAL",
439            StreamingAccountDataField::Equity => "EQUITY",
440            StreamingAccountDataField::EquityUsed => "EQUITY_USED",
441        };
442        write!(f, "{}", field_name)
443    }
444}
445
446impl Display for StreamingAccountDataField {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        write!(f, "{:?}", self)
449    }
450}
451
452/// Constructs a vector of serialized streaming account data field names from a given set of `StreamingAccountDataField`.
453///
454/// # Arguments
455///
456/// * `fields` - A reference to a `HashSet` containing `StreamingAccountDataField` items that need to be serialized.
457///
458/// # Returns
459///
460/// A `Vec<String>` where each `String` is the exact IG Lightstreamer wire name
461/// of a `StreamingAccountDataField` from the input set.
462pub(crate) fn get_streaming_account_data_fields(
463    fields: &HashSet<StreamingAccountDataField>,
464) -> Vec<String> {
465    // `Display` (via the `Debug` impl above) emits the exact IG Lightstreamer
466    // wire field name for every variant, so no fallible serialization is needed.
467    fields.iter().map(|field| field.to_string()).collect()
468}
469
470/// Streaming chart fields available for chart subscriptions (tick and candle).
471///
472/// These fields represent both tick-level and aggregated (candle) chart data
473/// provided by the IG Markets Lightstreamer streaming API.
474#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
475#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
476pub enum StreamingChartField {
477    // Common fields (available in both tick and candle data)
478    /// Last traded volume for the period (tick or candle)
479    #[default]
480    Ltv,
481    /// Incremental trading volume since last update
482    Ttv,
483    /// Update time as milliseconds from the Epoch
484    Utm,
485    /// Mid-market price at the start of the day
486    DayOpenMid,
487    /// Change from day's opening mid price to current mid price
488    DayNetChgMid,
489    /// Daily percentage change in mid price
490    DayPercChgMid,
491    /// Highest mid price for the day
492    DayHigh,
493    /// Lowest mid price for the day
494    DayLow,
495
496    // Tick-only fields (DISTINCT mode, CHART:{epic}:TICK)
497    /// Current bid price
498    Bid,
499    /// Current offer/ask price
500    Ofr,
501    /// Last traded price
502    Ltp,
503
504    // Candle-only fields (MERGE mode, CHART:{epic}:{scale})
505    /// Candle open price (offer)
506    OfrOpen,
507    /// Candle high price (offer)
508    OfrHigh,
509    /// Candle low price (offer)
510    OfrLow,
511    /// Candle closing price (offer)
512    OfrClose,
513    /// Candle open price (bid)
514    BidOpen,
515    /// Candle high price (bid)
516    BidHigh,
517    /// Candle low price (bid)
518    BidLow,
519    /// Candle closing price (bid)
520    BidClose,
521    /// Candle open price (last traded price)
522    LtpOpen,
523    /// Candle high price (last traded price)
524    LtpHigh,
525    /// Candle low price (last traded price)
526    LtpLow,
527    /// Candle closing price (last traded price)
528    LtpClose,
529    /// Indicator that candle ended (1 when candle ends, 0 otherwise)
530    ConsEnd,
531    /// Number of ticks consolidated in the candle
532    ConsTickCount,
533}
534
535impl std::fmt::Debug for StreamingChartField {
536    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537        let name = match self {
538            StreamingChartField::Ltv => "LTV",
539            StreamingChartField::Ttv => "TTV",
540            StreamingChartField::Utm => "UTM",
541            StreamingChartField::DayOpenMid => "DAY_OPEN_MID",
542            StreamingChartField::DayNetChgMid => "DAY_NET_CHG_MID",
543            StreamingChartField::DayPercChgMid => "DAY_PERC_CHG_MID",
544            StreamingChartField::DayHigh => "DAY_HIGH",
545            StreamingChartField::DayLow => "DAY_LOW",
546
547            StreamingChartField::Bid => "BID",
548            StreamingChartField::Ofr => "OFR",
549            StreamingChartField::Ltp => "LTP",
550
551            StreamingChartField::OfrOpen => "OFR_OPEN",
552            StreamingChartField::OfrHigh => "OFR_HIGH",
553            StreamingChartField::OfrLow => "OFR_LOW",
554            StreamingChartField::OfrClose => "OFR_CLOSE",
555            StreamingChartField::BidOpen => "BID_OPEN",
556            StreamingChartField::BidHigh => "BID_HIGH",
557            StreamingChartField::BidLow => "BID_LOW",
558            StreamingChartField::BidClose => "BID_CLOSE",
559            StreamingChartField::LtpOpen => "LTP_OPEN",
560            StreamingChartField::LtpHigh => "LTP_HIGH",
561            StreamingChartField::LtpLow => "LTP_LOW",
562            StreamingChartField::LtpClose => "LTP_CLOSE",
563            StreamingChartField::ConsEnd => "CONS_END",
564            StreamingChartField::ConsTickCount => "CONS_TICK_COUNT",
565        };
566        write!(f, "{}", name)
567    }
568}
569
570impl std::fmt::Display for StreamingChartField {
571    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572        write!(f, "{:?}", self)
573    }
574}
575
576/// Constructs a vector of serialized streaming chart field names from a given set of `StreamingChartField`.
577///
578/// # Arguments
579///
580/// * `fields` - A reference to a `HashSet` containing `StreamingChartField` items.
581///
582/// # Returns
583///
584/// A `Vec<String>` of exact IG Lightstreamer wire field names for
585/// subscriptions.
586pub(crate) fn get_streaming_chart_fields(fields: &HashSet<StreamingChartField>) -> Vec<String> {
587    // `Display` (via the `Debug` impl above) emits the exact IG Lightstreamer
588    // wire field name for every variant, so no fallible serialization is needed.
589    fields.iter().map(|field| field.to_string()).collect()
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn test_streaming_market_field_default() {
598        let field = StreamingMarketField::default();
599        assert_eq!(field, StreamingMarketField::Offer);
600    }
601
602    #[test]
603    fn test_streaming_market_field_debug() {
604        assert_eq!(format!("{:?}", StreamingMarketField::Bid), "BID");
605        assert_eq!(format!("{:?}", StreamingMarketField::Offer), "OFFER");
606        assert_eq!(format!("{:?}", StreamingMarketField::High), "HIGH");
607        assert_eq!(format!("{:?}", StreamingMarketField::Low), "LOW");
608        assert_eq!(format!("{:?}", StreamingMarketField::Change), "CHANGE");
609        assert_eq!(
610            format!("{:?}", StreamingMarketField::ChangePct),
611            "CHANGE_PCT"
612        );
613        assert_eq!(
614            format!("{:?}", StreamingMarketField::UpdateTime),
615            "UPDATE_TIME"
616        );
617        assert_eq!(
618            format!("{:?}", StreamingMarketField::MarketDelay),
619            "MARKET_DELAY"
620        );
621        assert_eq!(
622            format!("{:?}", StreamingMarketField::MarketState),
623            "MARKET_STATE"
624        );
625        assert_eq!(format!("{:?}", StreamingMarketField::MidOpen), "MID_OPEN");
626    }
627
628    #[test]
629    fn test_streaming_market_field_display() {
630        assert_eq!(format!("{}", StreamingMarketField::Bid), "BID");
631        assert_eq!(format!("{}", StreamingMarketField::Offer), "OFFER");
632    }
633
634    #[test]
635    fn test_get_streaming_market_fields_empty() {
636        let fields: HashSet<StreamingMarketField> = HashSet::new();
637        let result = get_streaming_market_fields(&fields);
638        assert!(result.is_empty());
639    }
640
641    #[test]
642    fn test_get_streaming_market_fields_single() {
643        let mut fields = HashSet::new();
644        fields.insert(StreamingMarketField::Bid);
645        let result = get_streaming_market_fields(&fields);
646        assert_eq!(result.len(), 1);
647        assert!(result.contains(&"BID".to_string()));
648    }
649
650    #[test]
651    fn test_get_streaming_market_fields_multiple() {
652        let mut fields = HashSet::new();
653        fields.insert(StreamingMarketField::Bid);
654        fields.insert(StreamingMarketField::Offer);
655        fields.insert(StreamingMarketField::High);
656        let result = get_streaming_market_fields(&fields);
657        assert_eq!(result.len(), 3);
658        assert!(result.contains(&"BID".to_string()));
659        assert!(result.contains(&"OFFER".to_string()));
660        assert!(result.contains(&"HIGH".to_string()));
661    }
662
663    #[test]
664    fn test_get_streaming_account_data_fields_wire_names() {
665        // Pins the exact IG Lightstreamer wire names produced by the getter
666        // after replacing the serde_json round-trip with Display, so a future
667        // divergence between Display and the wire contract fails loudly.
668        let mut fields = HashSet::new();
669        fields.insert(StreamingAccountDataField::Pnl);
670        fields.insert(StreamingAccountDataField::AvailableCash);
671        fields.insert(StreamingAccountDataField::PnlLr);
672        let result = get_streaming_account_data_fields(&fields);
673        assert_eq!(result.len(), 3);
674        assert!(result.contains(&"PNL".to_string()));
675        assert!(result.contains(&"AVAILABLE_CASH".to_string()));
676        assert!(result.contains(&"PNL_LR".to_string()));
677    }
678
679    #[test]
680    fn test_streaming_price_field_default() {
681        let field = StreamingPriceField::default();
682        assert_eq!(field, StreamingPriceField::AskPrice5);
683    }
684
685    #[test]
686    fn test_streaming_price_field_debug() {
687        assert_eq!(format!("{:?}", StreamingPriceField::High), "HIGH");
688        assert_eq!(format!("{:?}", StreamingPriceField::Low), "LOW");
689        assert_eq!(format!("{:?}", StreamingPriceField::MidOpen), "MID_OPEN");
690        assert_eq!(format!("{:?}", StreamingPriceField::BidPrice1), "BIDPRICE1");
691        assert_eq!(format!("{:?}", StreamingPriceField::AskPrice1), "ASKPRICE1");
692    }
693
694    #[test]
695    fn test_streaming_price_field_display() {
696        assert_eq!(format!("{}", StreamingPriceField::High), "HIGH");
697        assert_eq!(format!("{}", StreamingPriceField::BidPrice1), "BIDPRICE1");
698    }
699
700    #[test]
701    fn test_streaming_account_field_default() {
702        let field = StreamingAccountDataField::default();
703        assert_eq!(field, StreamingAccountDataField::Pnl);
704    }
705
706    #[test]
707    fn test_streaming_account_field_debug() {
708        assert_eq!(format!("{:?}", StreamingAccountDataField::Pnl), "PNL");
709        assert_eq!(
710            format!("{:?}", StreamingAccountDataField::Deposit),
711            "DEPOSIT"
712        );
713        assert_eq!(format!("{:?}", StreamingAccountDataField::Margin), "MARGIN");
714        assert_eq!(format!("{:?}", StreamingAccountDataField::Equity), "EQUITY");
715    }
716
717    #[test]
718    fn test_streaming_account_field_display() {
719        assert_eq!(format!("{}", StreamingAccountDataField::Pnl), "PNL");
720        assert_eq!(format!("{}", StreamingAccountDataField::Equity), "EQUITY");
721    }
722
723    #[test]
724    fn test_get_streaming_account_fields_empty() {
725        let fields: HashSet<StreamingAccountDataField> = HashSet::new();
726        let result = get_streaming_account_data_fields(&fields);
727        assert!(result.is_empty());
728    }
729
730    #[test]
731    fn test_get_streaming_account_fields_multiple() {
732        let mut fields = HashSet::new();
733        fields.insert(StreamingAccountDataField::Pnl);
734        fields.insert(StreamingAccountDataField::Equity);
735        let result = get_streaming_account_data_fields(&fields);
736        assert_eq!(result.len(), 2);
737        assert!(result.contains(&"PNL".to_string()));
738        assert!(result.contains(&"EQUITY".to_string()));
739    }
740
741    #[test]
742    fn test_streaming_chart_field_default() {
743        let field = StreamingChartField::default();
744        assert_eq!(field, StreamingChartField::Ltv);
745    }
746
747    #[test]
748    fn test_streaming_chart_field_debug() {
749        assert_eq!(format!("{:?}", StreamingChartField::Bid), "BID");
750        assert_eq!(format!("{:?}", StreamingChartField::Ofr), "OFR");
751        assert_eq!(format!("{:?}", StreamingChartField::Ltp), "LTP");
752        assert_eq!(format!("{:?}", StreamingChartField::Ltv), "LTV");
753        assert_eq!(format!("{:?}", StreamingChartField::Utm), "UTM");
754        assert_eq!(format!("{:?}", StreamingChartField::DayHigh), "DAY_HIGH");
755        assert_eq!(format!("{:?}", StreamingChartField::DayLow), "DAY_LOW");
756    }
757
758    #[test]
759    fn test_streaming_chart_field_display() {
760        assert_eq!(format!("{}", StreamingChartField::Bid), "BID");
761        assert_eq!(format!("{}", StreamingChartField::Ofr), "OFR");
762    }
763
764    #[test]
765    fn test_get_streaming_chart_fields_empty() {
766        let fields: HashSet<StreamingChartField> = HashSet::new();
767        let result = get_streaming_chart_fields(&fields);
768        assert!(result.is_empty());
769    }
770
771    #[test]
772    fn test_get_streaming_chart_fields_multiple() {
773        let mut fields = HashSet::new();
774        fields.insert(StreamingChartField::Bid);
775        fields.insert(StreamingChartField::Ofr);
776        fields.insert(StreamingChartField::Ltp);
777        let result = get_streaming_chart_fields(&fields);
778        assert_eq!(result.len(), 3);
779        assert!(result.contains(&"BID".to_string()));
780        assert!(result.contains(&"OFR".to_string()));
781        assert!(result.contains(&"LTP".to_string()));
782    }
783
784    #[test]
785    fn test_streaming_market_field_serialization() {
786        let field = StreamingMarketField::Bid;
787        let json = serde_json::to_string(&field).expect("serialize failed");
788        assert_eq!(json, "\"BID\"");
789
790        let deserialized: StreamingMarketField =
791            serde_json::from_str(&json).expect("deserialize failed");
792        assert_eq!(deserialized, StreamingMarketField::Bid);
793    }
794
795    #[test]
796    fn test_streaming_account_field_serialization() {
797        let field = StreamingAccountDataField::Pnl;
798        let json = serde_json::to_string(&field).expect("serialize failed");
799        assert_eq!(json, "\"PNL\"");
800
801        let deserialized: StreamingAccountDataField =
802            serde_json::from_str(&json).expect("deserialize failed");
803        assert_eq!(deserialized, StreamingAccountDataField::Pnl);
804    }
805
806    #[test]
807    fn test_streaming_chart_field_serialization() {
808        let field = StreamingChartField::Bid;
809        let json = serde_json::to_string(&field).expect("serialize failed");
810        assert_eq!(json, "\"BID\"");
811
812        let deserialized: StreamingChartField =
813            serde_json::from_str(&json).expect("deserialize failed");
814        assert_eq!(deserialized, StreamingChartField::Bid);
815    }
816
817    #[test]
818    fn test_streaming_market_field_equality() {
819        let field1 = StreamingMarketField::Bid;
820        let field2 = StreamingMarketField::Bid;
821        let field3 = StreamingMarketField::Offer;
822
823        assert_eq!(field1, field2);
824        assert_ne!(field1, field3);
825    }
826
827    #[test]
828    fn test_streaming_market_field_hash() {
829        let mut set = HashSet::new();
830        set.insert(StreamingMarketField::Bid);
831        set.insert(StreamingMarketField::Bid); // Duplicate
832
833        assert_eq!(set.len(), 1);
834    }
835
836    #[test]
837    fn test_streaming_market_field_clone() {
838        let field = StreamingMarketField::High;
839        let cloned = field.clone();
840        assert_eq!(field, cloned);
841    }
842
843    // Exhaustive wire-name pinning for every enum variant. Each variant's
844    // JSON serialization, `Debug`, and `Display` must all agree on the exact
845    // IG Lightstreamer field name, so any future rename fails loudly. This is
846    // the coverage previously carried by `tests/unit/model/test_streaming.rs`.
847    fn assert_wire_name<T>(field: &T, expected: &str)
848    where
849        T: Serialize + Debug + Display,
850    {
851        let serialized = serde_json::to_string(field).expect("serialize failed");
852        assert_eq!(serialized, format!("\"{expected}\""));
853        assert_eq!(format!("{field:?}"), expected);
854        assert_eq!(format!("{field}"), expected);
855    }
856
857    #[test]
858    fn test_streaming_market_field_all_variants_wire_names() {
859        for (field, expected) in [
860            (StreamingMarketField::MidOpen, "MID_OPEN"),
861            (StreamingMarketField::High, "HIGH"),
862            (StreamingMarketField::Low, "LOW"),
863            (StreamingMarketField::Change, "CHANGE"),
864            (StreamingMarketField::ChangePct, "CHANGE_PCT"),
865            (StreamingMarketField::UpdateTime, "UPDATE_TIME"),
866            (StreamingMarketField::MarketDelay, "MARKET_DELAY"),
867            (StreamingMarketField::MarketState, "MARKET_STATE"),
868            (StreamingMarketField::Bid, "BID"),
869            (StreamingMarketField::Offer, "OFFER"),
870        ] {
871            assert_wire_name(&field, expected);
872        }
873    }
874
875    #[test]
876    fn test_streaming_account_field_all_variants_wire_names() {
877        for (field, expected) in [
878            (StreamingAccountDataField::Pnl, "PNL"),
879            (StreamingAccountDataField::Deposit, "DEPOSIT"),
880            (StreamingAccountDataField::AvailableCash, "AVAILABLE_CASH"),
881            (StreamingAccountDataField::PnlLr, "PNL_LR"),
882            (StreamingAccountDataField::PnlNlr, "PNL_NLR"),
883            (StreamingAccountDataField::Funds, "FUNDS"),
884            (StreamingAccountDataField::Margin, "MARGIN"),
885            (StreamingAccountDataField::MarginLr, "MARGIN_LR"),
886            (StreamingAccountDataField::MarginNlr, "MARGIN_NLR"),
887            (
888                StreamingAccountDataField::AvailableToDeal,
889                "AVAILABLE_TO_DEAL",
890            ),
891            (StreamingAccountDataField::Equity, "EQUITY"),
892            (StreamingAccountDataField::EquityUsed, "EQUITY_USED"),
893        ] {
894            assert_wire_name(&field, expected);
895        }
896    }
897
898    #[test]
899    fn test_streaming_price_field_special_variants_wire_names() {
900        // Pins the variants whose wire names are non-obvious: explicit renames
901        // (MID_OPEN, DLG_FLAG) and the UPPERCASE-run collapses (BIDQUOTEID,
902        // BIDPRICE1, C1BIDSIZE1, ...).
903        for (field, expected) in [
904            (StreamingPriceField::MidOpen, "MID_OPEN"),
905            (StreamingPriceField::High, "HIGH"),
906            (StreamingPriceField::Low, "LOW"),
907            (StreamingPriceField::BidQuoteId, "BIDQUOTEID"),
908            (StreamingPriceField::AskQuoteId, "ASKQUOTEID"),
909            (StreamingPriceField::BidPrice1, "BIDPRICE1"),
910            (StreamingPriceField::AskPrice5, "ASKPRICE5"),
911            (StreamingPriceField::Currency0, "CURRENCY0"),
912            (StreamingPriceField::C1BidSize1, "C1BIDSIZE1"),
913            (StreamingPriceField::C5AskSize5, "C5ASKSIZE5"),
914            (StreamingPriceField::Timestamp, "TIMESTAMP"),
915            (StreamingPriceField::DlgFlag, "DLG_FLAG"),
916        ] {
917            assert_wire_name(&field, expected);
918        }
919    }
920
921    #[test]
922    fn test_streaming_chart_field_all_variants_wire_names() {
923        for (field, expected) in [
924            (StreamingChartField::Ltv, "LTV"),
925            (StreamingChartField::Ttv, "TTV"),
926            (StreamingChartField::Utm, "UTM"),
927            (StreamingChartField::DayOpenMid, "DAY_OPEN_MID"),
928            (StreamingChartField::DayNetChgMid, "DAY_NET_CHG_MID"),
929            (StreamingChartField::DayPercChgMid, "DAY_PERC_CHG_MID"),
930            (StreamingChartField::DayHigh, "DAY_HIGH"),
931            (StreamingChartField::DayLow, "DAY_LOW"),
932            (StreamingChartField::Bid, "BID"),
933            (StreamingChartField::Ofr, "OFR"),
934            (StreamingChartField::Ltp, "LTP"),
935            (StreamingChartField::OfrOpen, "OFR_OPEN"),
936            (StreamingChartField::OfrHigh, "OFR_HIGH"),
937            (StreamingChartField::OfrLow, "OFR_LOW"),
938            (StreamingChartField::OfrClose, "OFR_CLOSE"),
939            (StreamingChartField::BidOpen, "BID_OPEN"),
940            (StreamingChartField::BidHigh, "BID_HIGH"),
941            (StreamingChartField::BidLow, "BID_LOW"),
942            (StreamingChartField::BidClose, "BID_CLOSE"),
943            (StreamingChartField::LtpOpen, "LTP_OPEN"),
944            (StreamingChartField::LtpHigh, "LTP_HIGH"),
945            (StreamingChartField::LtpLow, "LTP_LOW"),
946            (StreamingChartField::LtpClose, "LTP_CLOSE"),
947            (StreamingChartField::ConsEnd, "CONS_END"),
948            (StreamingChartField::ConsTickCount, "CONS_TICK_COUNT"),
949        ] {
950            assert_wire_name(&field, expected);
951        }
952    }
953
954    /// Every variant of every streaming field enum, so drift between the serde
955    /// renames and the `Display`/`Debug` wire mapping can be checked
956    /// exhaustively (see [`test_all_streaming_fields_serde_matches_display`]).
957    const ALL_MARKET_FIELDS: &[StreamingMarketField] = &[
958        StreamingMarketField::MidOpen,
959        StreamingMarketField::High,
960        StreamingMarketField::Low,
961        StreamingMarketField::Change,
962        StreamingMarketField::ChangePct,
963        StreamingMarketField::UpdateTime,
964        StreamingMarketField::MarketDelay,
965        StreamingMarketField::MarketState,
966        StreamingMarketField::Bid,
967        StreamingMarketField::Offer,
968    ];
969
970    const ALL_ACCOUNT_FIELDS: &[StreamingAccountDataField] = &[
971        StreamingAccountDataField::Pnl,
972        StreamingAccountDataField::Deposit,
973        StreamingAccountDataField::AvailableCash,
974        StreamingAccountDataField::PnlLr,
975        StreamingAccountDataField::PnlNlr,
976        StreamingAccountDataField::Funds,
977        StreamingAccountDataField::Margin,
978        StreamingAccountDataField::MarginLr,
979        StreamingAccountDataField::MarginNlr,
980        StreamingAccountDataField::AvailableToDeal,
981        StreamingAccountDataField::Equity,
982        StreamingAccountDataField::EquityUsed,
983    ];
984
985    const ALL_CHART_FIELDS: &[StreamingChartField] = &[
986        StreamingChartField::Ltv,
987        StreamingChartField::Ttv,
988        StreamingChartField::Utm,
989        StreamingChartField::DayOpenMid,
990        StreamingChartField::DayNetChgMid,
991        StreamingChartField::DayPercChgMid,
992        StreamingChartField::DayHigh,
993        StreamingChartField::DayLow,
994        StreamingChartField::Bid,
995        StreamingChartField::Ofr,
996        StreamingChartField::Ltp,
997        StreamingChartField::OfrOpen,
998        StreamingChartField::OfrHigh,
999        StreamingChartField::OfrLow,
1000        StreamingChartField::OfrClose,
1001        StreamingChartField::BidOpen,
1002        StreamingChartField::BidHigh,
1003        StreamingChartField::BidLow,
1004        StreamingChartField::BidClose,
1005        StreamingChartField::LtpOpen,
1006        StreamingChartField::LtpHigh,
1007        StreamingChartField::LtpLow,
1008        StreamingChartField::LtpClose,
1009        StreamingChartField::ConsEnd,
1010        StreamingChartField::ConsTickCount,
1011    ];
1012
1013    const ALL_PRICE_FIELDS: &[StreamingPriceField] = &[
1014        StreamingPriceField::MidOpen,
1015        StreamingPriceField::High,
1016        StreamingPriceField::Low,
1017        StreamingPriceField::BidQuoteId,
1018        StreamingPriceField::AskQuoteId,
1019        StreamingPriceField::BidPrice1,
1020        StreamingPriceField::BidPrice2,
1021        StreamingPriceField::BidPrice3,
1022        StreamingPriceField::BidPrice4,
1023        StreamingPriceField::BidPrice5,
1024        StreamingPriceField::AskPrice1,
1025        StreamingPriceField::AskPrice2,
1026        StreamingPriceField::AskPrice3,
1027        StreamingPriceField::AskPrice4,
1028        StreamingPriceField::AskPrice5,
1029        StreamingPriceField::BidSize1,
1030        StreamingPriceField::BidSize2,
1031        StreamingPriceField::BidSize3,
1032        StreamingPriceField::BidSize4,
1033        StreamingPriceField::BidSize5,
1034        StreamingPriceField::AskSize1,
1035        StreamingPriceField::AskSize2,
1036        StreamingPriceField::AskSize3,
1037        StreamingPriceField::AskSize4,
1038        StreamingPriceField::AskSize5,
1039        StreamingPriceField::Currency0,
1040        StreamingPriceField::Currency1,
1041        StreamingPriceField::C1BidSize1,
1042        StreamingPriceField::C1BidSize2,
1043        StreamingPriceField::C1BidSize3,
1044        StreamingPriceField::C1BidSize4,
1045        StreamingPriceField::C1BidSize5,
1046        StreamingPriceField::C1AskSize1,
1047        StreamingPriceField::C1AskSize2,
1048        StreamingPriceField::C1AskSize3,
1049        StreamingPriceField::C1AskSize4,
1050        StreamingPriceField::C1AskSize5,
1051        StreamingPriceField::Currency2,
1052        StreamingPriceField::C2BidSize1,
1053        StreamingPriceField::C2BidSize2,
1054        StreamingPriceField::C2BidSize3,
1055        StreamingPriceField::C2BidSize4,
1056        StreamingPriceField::C2BidSize5,
1057        StreamingPriceField::C2AskSize1,
1058        StreamingPriceField::C2AskSize2,
1059        StreamingPriceField::C2AskSize3,
1060        StreamingPriceField::C2AskSize4,
1061        StreamingPriceField::C2AskSize5,
1062        StreamingPriceField::Currency3,
1063        StreamingPriceField::C3BidSize1,
1064        StreamingPriceField::C3BidSize2,
1065        StreamingPriceField::C3BidSize3,
1066        StreamingPriceField::C3BidSize4,
1067        StreamingPriceField::C3BidSize5,
1068        StreamingPriceField::C3AskSize1,
1069        StreamingPriceField::C3AskSize2,
1070        StreamingPriceField::C3AskSize3,
1071        StreamingPriceField::C3AskSize4,
1072        StreamingPriceField::C3AskSize5,
1073        StreamingPriceField::Currency4,
1074        StreamingPriceField::C4BidSize1,
1075        StreamingPriceField::C4BidSize2,
1076        StreamingPriceField::C4BidSize3,
1077        StreamingPriceField::C4BidSize4,
1078        StreamingPriceField::C4BidSize5,
1079        StreamingPriceField::C4AskSize1,
1080        StreamingPriceField::C4AskSize2,
1081        StreamingPriceField::C4AskSize3,
1082        StreamingPriceField::C4AskSize4,
1083        StreamingPriceField::C4AskSize5,
1084        StreamingPriceField::Currency5,
1085        StreamingPriceField::C5BidSize1,
1086        StreamingPriceField::C5BidSize2,
1087        StreamingPriceField::C5BidSize3,
1088        StreamingPriceField::C5BidSize4,
1089        StreamingPriceField::C5BidSize5,
1090        StreamingPriceField::C5AskSize1,
1091        StreamingPriceField::C5AskSize2,
1092        StreamingPriceField::C5AskSize3,
1093        StreamingPriceField::C5AskSize4,
1094        StreamingPriceField::C5AskSize5,
1095        StreamingPriceField::Timestamp,
1096        StreamingPriceField::DlgFlag,
1097        StreamingPriceField::NetChg,
1098        StreamingPriceField::NetChgPct,
1099        StreamingPriceField::Delay,
1100    ];
1101
1102    /// Asserts that the serde wire name (the JSON string a variant serializes
1103    /// to) matches the variant's `Display` output. This is the single guard
1104    /// that keeps the serde renames and the `Display`/`Debug` mapping — the two
1105    /// independent sources for the IG Lightstreamer wire name — from drifting.
1106    fn assert_serde_matches_display<T>(field: &T)
1107    where
1108        T: Serialize + Display,
1109    {
1110        let value = serde_json::to_value(field).expect("serialize to value failed");
1111        let wire = value
1112            .as_str()
1113            .expect("a streaming field enum variant must serialize to a JSON string");
1114        assert_eq!(
1115            wire,
1116            field.to_string(),
1117            "serde wire name and Display disagree for a streaming field variant"
1118        );
1119    }
1120
1121    #[test]
1122    fn test_all_streaming_fields_serde_matches_display() {
1123        for field in ALL_MARKET_FIELDS {
1124            assert_serde_matches_display(field);
1125        }
1126        for field in ALL_ACCOUNT_FIELDS {
1127            assert_serde_matches_display(field);
1128        }
1129        for field in ALL_CHART_FIELDS {
1130            assert_serde_matches_display(field);
1131        }
1132        for field in ALL_PRICE_FIELDS {
1133            assert_serde_matches_display(field);
1134        }
1135    }
1136
1137    #[test]
1138    fn test_get_streaming_price_fields_uses_display_wire_names() {
1139        // The price getter now derives wire names from `Display`; pin a couple of
1140        // non-obvious ones so a regression to a divergent mapping fails loudly.
1141        let mut fields = HashSet::new();
1142        fields.insert(StreamingPriceField::MidOpen);
1143        fields.insert(StreamingPriceField::C1BidSize1);
1144        fields.insert(StreamingPriceField::DlgFlag);
1145        let result = get_streaming_price_fields(&fields);
1146        assert_eq!(result.len(), 3);
1147        assert!(result.contains(&"MID_OPEN".to_string()));
1148        assert!(result.contains(&"C1BIDSIZE1".to_string()));
1149        assert!(result.contains(&"DLG_FLAG".to_string()));
1150    }
1151
1152    #[test]
1153    fn test_streaming_price_field_in_hashset() {
1154        let mut set = HashSet::new();
1155        set.insert(StreamingPriceField::MidOpen);
1156        set.insert(StreamingPriceField::High);
1157        set.insert(StreamingPriceField::MidOpen); // duplicate
1158        assert_eq!(set.len(), 2);
1159        assert!(set.contains(&StreamingPriceField::MidOpen));
1160        assert!(!set.contains(&StreamingPriceField::Low));
1161    }
1162
1163    #[test]
1164    fn test_streaming_chart_field_in_hashset() {
1165        let mut set = HashSet::new();
1166        set.insert(StreamingChartField::Bid);
1167        set.insert(StreamingChartField::Ofr);
1168        set.insert(StreamingChartField::Bid); // duplicate
1169        assert_eq!(set.len(), 2);
1170        assert!(set.contains(&StreamingChartField::Bid));
1171        assert!(!set.contains(&StreamingChartField::Ltp));
1172    }
1173
1174    #[test]
1175    fn test_streaming_account_field_in_hashset() {
1176        let mut set = HashSet::new();
1177        set.insert(StreamingAccountDataField::Pnl);
1178        set.insert(StreamingAccountDataField::Deposit);
1179        set.insert(StreamingAccountDataField::Pnl); // duplicate
1180        assert_eq!(set.len(), 2);
1181        assert!(set.contains(&StreamingAccountDataField::Pnl));
1182        assert!(!set.contains(&StreamingAccountDataField::AvailableCash));
1183    }
1184}