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