ig_client/model/
streaming.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 25/10/25
5******************************************************************************/
6
7//! Streaming data field definitions for IG Markets API.
8//!
9//! This module provides enums and helper functions for working with streaming
10//! subscriptions in the IG Markets API. It includes field definitions for:
11//! - Market data (prices, market state)
12//! - Price data (detailed bid/ask levels)
13//! - Account data (P&L, margin, equity)
14
15use crate::prelude::{Deserialize, Serialize};
16use std::collections::HashSet;
17use std::fmt::{Debug, Display};
18
19/// Streaming market fields available for market subscriptions.
20///
21/// These fields represent the various market data points that can be subscribed to
22/// in the IG Markets streaming API for market updates.
23#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
24#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
25pub enum StreamingMarketField {
26    /// Mid open price
27    MidOpen,
28    /// High price
29    High,
30    /// Low price
31    Low,
32    /// Price change
33    Change,
34    /// Percentage change
35    ChangePct,
36    /// Last update time
37    UpdateTime,
38    /// Market delay in milliseconds
39    MarketDelay,
40    /// Market state (e.g., TRADEABLE, CLOSED)
41    MarketState,
42    /// Bid price
43    Bid,
44    /// Offer/Ask price
45    #[default]
46    Offer,
47}
48
49impl Debug for StreamingMarketField {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        let field_name = serde_json::to_string(self).unwrap();
52        write!(f, "{:?}", field_name)
53    }
54}
55
56impl Display for StreamingMarketField {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{:?}", self)
59    }
60}
61
62/// Constructs a vector of serialized streaming market field names from a given set of `StreamingMarketField`.
63///
64/// # Arguments
65///
66/// * `fields` - A reference to a `HashSet` containing `StreamingMarketField` items that need to be serialized.
67///
68/// # Returns
69///
70/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingMarketField` from the input set.
71///
72/// # Panics
73///
74/// This function will panic if the serialization of any `StreamingMarketField` fails.
75///
76pub(crate) fn get_streaming_market_fields(fields: &HashSet<StreamingMarketField>) -> Vec<String> {
77    let mut fields_vec = Vec::new();
78    for field in fields {
79        // Serialize to a JSON value and extract the underlying string without quotes
80        let val = serde_json::to_value(field).expect("Failed to serialize StreamingMarketField");
81        match val {
82            serde_json::Value::String(s) => fields_vec.push(s),
83            // Fallback: use Debug which yields SCREAMING_SNAKE_CASE variant name
84            _ => fields_vec.push(format!("{:?}", field)),
85        }
86    }
87    fields_vec
88}
89
90/// Streaming price fields available for price subscriptions.
91///
92/// These fields represent the various price data points that can be subscribed to
93/// in the IG Markets streaming API for price updates.
94#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
95#[serde(rename_all = "UPPERCASE")]
96pub enum StreamingPriceField {
97    /// Mid open price
98    #[serde(rename = "MID_OPEN")]
99    MidOpen,
100    /// High price
101    High,
102    /// Low price
103    Low,
104    /// Bid quote ID
105    BidQuoteId,
106    /// Ask quote ID
107    AskQuoteId,
108    /// Bid price level 1
109    BidPrice1,
110    /// Bid price level 2
111    BidPrice2,
112    /// Bid price level 3
113    BidPrice3,
114    /// Bid price level 4
115    BidPrice4,
116    /// Bid price level 5
117    BidPrice5,
118    /// Ask price level 1
119    AskPrice1,
120    /// Ask price level 2
121    AskPrice2,
122    /// Ask price level 3
123    AskPrice3,
124    /// Ask price level 4
125    AskPrice4,
126    /// Ask price level 5
127    #[default]
128    AskPrice5,
129    /// Bid size level 1
130    BidSize1,
131    /// Bid size level 2
132    BidSize2,
133    /// Bid size level 3
134    BidSize3,
135    /// Bid size level 4
136    BidSize4,
137    /// Bid size level 5
138    BidSize5,
139    /// Ask size level 1
140    AskSize1,
141    /// Ask size level 2
142    AskSize2,
143    /// Ask size level 3
144    AskSize3,
145    /// Ask size level 4
146    AskSize4,
147    /// Ask size level 5
148    AskSize5,
149    /// Currency 0
150    Currency0,
151    /// Currency 1
152    Currency1,
153    /// Currency 1 bid size level 1
154    C1BidSize1,
155    /// Currency 1 bid size level 2
156    C1BidSize2,
157    /// Currency 1 bid size level 3
158    C1BidSize3,
159    /// Currency 1 bid size level 4
160    C1BidSize4,
161    /// Currency 1 bid size level 5
162    C1BidSize5,
163    /// Currency 1 ask size level 1
164    C1AskSize1,
165    /// Currency 1 ask size level 2
166    C1AskSize2,
167    /// Currency 1 ask size level 3
168    C1AskSize3,
169    /// Currency 1 ask size level 4
170    C1AskSize4,
171    /// Currency 1 ask size level 5
172    C1AskSize5,
173    /// Currency 2
174    Currency2,
175    /// Currency 2 bid size level 1
176    C2BidSize1,
177    /// Currency 2 bid size level 2
178    C2BidSize2,
179    /// Currency 2 bid size level 3
180    C2BidSize3,
181    /// Currency 2 bid size level 4
182    C2BidSize4,
183    /// Currency 2 bid size level 5
184    C2BidSize5,
185    /// Currency 2 ask size level 1
186    C2AskSize1,
187    /// Currency 2 ask size level 2
188    C2AskSize2,
189    /// Currency 2 ask size level 3
190    C2AskSize3,
191    /// Currency 2 ask size level 4
192    C2AskSize4,
193    /// Currency 2 ask size level 5
194    C2AskSize5,
195    /// Currency 3
196    Currency3,
197    /// Currency 3 bid size level 1
198    C3BidSize1,
199    /// Currency 3 bid size level 2
200    C3BidSize2,
201    /// Currency 3 bid size level 3
202    C3BidSize3,
203    /// Currency 3 bid size level 4
204    C3BidSize4,
205    /// Currency 3 bid size level 5
206    C3BidSize5,
207    /// Currency 3 ask size level 1
208    C3AskSize1,
209    /// Currency 3 ask size level 2
210    C3AskSize2,
211    /// Currency 3 ask size level 3
212    C3AskSize3,
213    /// Currency 3 ask size level 4
214    C3AskSize4,
215    /// Currency 3 ask size level 5
216    C3AskSize5,
217    /// Currency 4
218    Currency4,
219    /// Currency 4 bid size level 1
220    C4BidSize1,
221    /// Currency 4 bid size level 2
222    C4BidSize2,
223    /// Currency 4 bid size level 3
224    C4BidSize3,
225    /// Currency 4 bid size level 4
226    C4BidSize4,
227    /// Currency 4 bid size level 5
228    C4BidSize5,
229    /// Currency 4 ask size level 1
230    C4AskSize1,
231    /// Currency 4 ask size level 2
232    C4AskSize2,
233    /// Currency 4 ask size level 3
234    C4AskSize3,
235    /// Currency 4 ask size level 4
236    C4AskSize4,
237    /// Currency 4 ask size level 5
238    C4AskSize5,
239    /// Currency 5
240    Currency5,
241    /// Currency 5 bid size level 1
242    C5BidSize1,
243    /// Currency 5 bid size level 2
244    C5BidSize2,
245    /// Currency 5 bid size level 3
246    C5BidSize3,
247    /// Currency 5 bid size level 4
248    C5BidSize4,
249    /// Currency 5 bid size level 5
250    C5BidSize5,
251    /// Currency 5 ask size level 1
252    C5AskSize1,
253    /// Currency 5 ask size level 2
254    C5AskSize2,
255    /// Currency 5 ask size level 3
256    C5AskSize3,
257    /// Currency 5 ask size level 4
258    C5AskSize4,
259    /// Currency 5 ask size level 5
260    C5AskSize5,
261    /// Timestamp of the price update
262    Timestamp,
263    /// Dealing flag
264    #[serde(rename = "DLG_FLAG")]
265    DlgFlag,
266}
267
268impl Debug for StreamingPriceField {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        let field_name = serde_json::to_string(self).unwrap();
271        write!(f, "{:?}", field_name)
272    }
273}
274
275impl Display for StreamingPriceField {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        write!(f, "{:?}", self)
278    }
279}
280
281/// Constructs a vector of serialized streaming price field names from a given set of `StreamingPriceField`.
282///
283/// # Arguments
284///
285/// * `fields` - A reference to a `HashSet` containing `StreamingPriceField` items that need to be serialized.
286///
287/// # Returns
288///
289/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingPriceField` from the input set.
290///
291/// # Panics
292///
293/// This function will panic if the serialization of any `StreamingPriceField` fails.
294///
295pub(crate) fn get_streaming_price_fields(fields: &HashSet<StreamingPriceField>) -> Vec<String> {
296    // Map each enum variant to the exact IG Lightstreamer field identifier.
297    let map_field = |f: &StreamingPriceField| -> &'static str {
298        match f {
299            // Core prices
300            StreamingPriceField::MidOpen => "MID_OPEN",
301            StreamingPriceField::High => "HIGH",
302            StreamingPriceField::Low => "LOW",
303            StreamingPriceField::BidQuoteId => "BIDQUOTEID",
304            StreamingPriceField::AskQuoteId => "ASKQUOTEID",
305
306            // Bid ladder prices
307            StreamingPriceField::BidPrice1 => "BIDPRICE1",
308            StreamingPriceField::BidPrice2 => "BIDPRICE2",
309            StreamingPriceField::BidPrice3 => "BIDPRICE3",
310            StreamingPriceField::BidPrice4 => "BIDPRICE4",
311            StreamingPriceField::BidPrice5 => "BIDPRICE5",
312
313            // Ask ladder prices
314            StreamingPriceField::AskPrice1 => "ASKPRICE1",
315            StreamingPriceField::AskPrice2 => "ASKPRICE2",
316            StreamingPriceField::AskPrice3 => "ASKPRICE3",
317            StreamingPriceField::AskPrice4 => "ASKPRICE4",
318            StreamingPriceField::AskPrice5 => "ASKPRICE5",
319
320            // Bid sizes
321            StreamingPriceField::BidSize1 => "BIDSIZE1",
322            StreamingPriceField::BidSize2 => "BIDSIZE2",
323            StreamingPriceField::BidSize3 => "BIDSIZE3",
324            StreamingPriceField::BidSize4 => "BIDSIZE4",
325            StreamingPriceField::BidSize5 => "BIDSIZE5",
326
327            // Ask sizes
328            StreamingPriceField::AskSize1 => "ASKSIZE1",
329            StreamingPriceField::AskSize2 => "ASKSIZE2",
330            StreamingPriceField::AskSize3 => "ASKSIZE3",
331            StreamingPriceField::AskSize4 => "ASKSIZE4",
332            StreamingPriceField::AskSize5 => "ASKSIZE5",
333
334            // Currencies
335            StreamingPriceField::Currency0 => "CURRENCY0",
336            StreamingPriceField::Currency1 => "CURRENCY1",
337            StreamingPriceField::Currency2 => "CURRENCY2",
338            StreamingPriceField::Currency3 => "CURRENCY3",
339            StreamingPriceField::Currency4 => "CURRENCY4",
340            StreamingPriceField::Currency5 => "CURRENCY5",
341
342            // Currency 1 bid sizes
343            StreamingPriceField::C1BidSize1 => "C1BIDSIZE1",
344            StreamingPriceField::C1BidSize2 => "C1BIDSIZE2",
345            StreamingPriceField::C1BidSize3 => "C1BIDSIZE3",
346            StreamingPriceField::C1BidSize4 => "C1BIDSIZE4",
347            StreamingPriceField::C1BidSize5 => "C1BIDSIZE5",
348            // Currency 1 ask sizes
349            StreamingPriceField::C1AskSize1 => "C1ASKSIZE1",
350            StreamingPriceField::C1AskSize2 => "C1ASKSIZE2",
351            StreamingPriceField::C1AskSize3 => "C1ASKSIZE3",
352            StreamingPriceField::C1AskSize4 => "C1ASKSIZE4",
353            StreamingPriceField::C1AskSize5 => "C1ASKSIZE5",
354
355            // Currency 2 bid sizes
356            StreamingPriceField::C2BidSize1 => "C2BIDSIZE1",
357            StreamingPriceField::C2BidSize2 => "C2BIDSIZE2",
358            StreamingPriceField::C2BidSize3 => "C2BIDSIZE3",
359            StreamingPriceField::C2BidSize4 => "C2BIDSIZE4",
360            StreamingPriceField::C2BidSize5 => "C2BIDSIZE5",
361            // Currency 2 ask sizes
362            StreamingPriceField::C2AskSize1 => "C2ASKSIZE1",
363            StreamingPriceField::C2AskSize2 => "C2ASKSIZE2",
364            StreamingPriceField::C2AskSize3 => "C2ASKSIZE3",
365            StreamingPriceField::C2AskSize4 => "C2ASKSIZE4",
366            StreamingPriceField::C2AskSize5 => "C2ASKSIZE5",
367
368            // Currency 3 bid sizes
369            StreamingPriceField::C3BidSize1 => "C3BIDSIZE1",
370            StreamingPriceField::C3BidSize2 => "C3BIDSIZE2",
371            StreamingPriceField::C3BidSize3 => "C3BIDSIZE3",
372            StreamingPriceField::C3BidSize4 => "C3BIDSIZE4",
373            StreamingPriceField::C3BidSize5 => "C3BIDSIZE5",
374            // Currency 3 ask sizes
375            StreamingPriceField::C3AskSize1 => "C3ASKSIZE1",
376            StreamingPriceField::C3AskSize2 => "C3ASKSIZE2",
377            StreamingPriceField::C3AskSize3 => "C3ASKSIZE3",
378            StreamingPriceField::C3AskSize4 => "C3ASKSIZE4",
379            StreamingPriceField::C3AskSize5 => "C3ASKSIZE5",
380
381            // Currency 4 bid sizes
382            StreamingPriceField::C4BidSize1 => "C4BIDSIZE1",
383            StreamingPriceField::C4BidSize2 => "C4BIDSIZE2",
384            StreamingPriceField::C4BidSize3 => "C4BIDSIZE3",
385            StreamingPriceField::C4BidSize4 => "C4BIDSIZE4",
386            StreamingPriceField::C4BidSize5 => "C4BIDSIZE5",
387            // Currency 4 ask sizes
388            StreamingPriceField::C4AskSize1 => "C4ASKSIZE1",
389            StreamingPriceField::C4AskSize2 => "C4ASKSIZE2",
390            StreamingPriceField::C4AskSize3 => "C4ASKSIZE3",
391            StreamingPriceField::C4AskSize4 => "C4ASKSIZE4",
392            StreamingPriceField::C4AskSize5 => "C4ASKSIZE5",
393
394            // Currency 5 bid sizes
395            StreamingPriceField::C5BidSize1 => "C5BIDSIZE1",
396            StreamingPriceField::C5BidSize2 => "C5BIDSIZE2",
397            StreamingPriceField::C5BidSize3 => "C5BIDSIZE3",
398            StreamingPriceField::C5BidSize4 => "C5BIDSIZE4",
399            StreamingPriceField::C5BidSize5 => "C5BIDSIZE5",
400            // Currency 5 ask sizes
401            StreamingPriceField::C5AskSize1 => "C5ASKSIZE1",
402            StreamingPriceField::C5AskSize2 => "C5ASKSIZE2",
403            StreamingPriceField::C5AskSize3 => "C5ASKSIZE3",
404            StreamingPriceField::C5AskSize4 => "C5ASKSIZE4",
405            StreamingPriceField::C5AskSize5 => "C5ASKSIZE5",
406
407            // Misc
408            StreamingPriceField::Timestamp => "TIMESTAMP",
409            StreamingPriceField::DlgFlag => "DLG_FLAG",
410        }
411    };
412
413    let mut fields_vec = Vec::with_capacity(fields.len());
414    for field in fields {
415        fields_vec.push(map_field(field).to_string());
416    }
417    fields_vec
418}
419
420/// Streaming account data fields available for account subscriptions.
421///
422/// These fields represent the various account data points that can be subscribed to
423/// in the IG Markets streaming API for account updates.
424#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Default, Hash)]
425#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
426pub enum StreamingAccountDataField {
427    /// Profit and loss
428    #[default]
429    Pnl,
430    /// Deposit amount
431    Deposit,
432    /// Available cash
433    AvailableCash,
434    /// Profit and loss for long positions with guaranteed stops
435    PnlLr,
436    /// Profit and loss for long positions without guaranteed stops
437    PnlNlr,
438    /// Total funds
439    Funds,
440    /// Total margin
441    Margin,
442    /// Margin for positions with guaranteed stops
443    MarginLr,
444    /// Margin for positions without guaranteed stops
445    MarginNlr,
446    /// Available amount to deal
447    AvailableToDeal,
448    /// Total equity
449    Equity,
450    /// Equity used
451    EquityUsed,
452}
453
454impl Debug for StreamingAccountDataField {
455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456        let field_name = serde_json::to_string(self).unwrap();
457        write!(f, "{:?}", field_name)
458    }
459}
460
461impl Display for StreamingAccountDataField {
462    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463        write!(f, "{:?}", self)
464    }
465}
466
467/// Constructs a vector of serialized streaming account data field names from a given set of `StreamingAccountDataField`.
468///
469/// # Arguments
470///
471/// * `fields` - A reference to a `HashSet` containing `StreamingAccountDataField` items that need to be serialized.
472///
473/// # Returns
474///
475/// A `Vec<String>` where each `String` is a serialized representation of a `StreamingAccountDataField` from the input set.
476///
477/// # Panics
478///
479/// This function will panic if the serialization of any `StreamingAccountDataField` fails.
480///
481pub(crate) fn get_streaming_account_data_fields(
482    fields: &HashSet<StreamingAccountDataField>,
483) -> Vec<String> {
484    let mut fields_vec = Vec::new();
485    for field in fields {
486        let val =
487            serde_json::to_value(field).expect("Failed to serialize StreamingAccountDataField");
488        match val {
489            serde_json::Value::String(s) => fields_vec.push(s),
490            _ => fields_vec.push(format!("{:?}", field)),
491        }
492    }
493    fields_vec
494}