ig_client/presentation/
price.rs

1use crate::presentation::serialization::string_as_float_opt;
2use lightstreamer_rs::subscription::ItemUpdate;
3use pretty_simple_display::{DebugPretty, DisplaySimple};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Market dealing status flags indicating trading availability
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
9#[serde(rename_all = "UPPERCASE")]
10pub enum DealingFlag {
11    /// Market is closed for trading
12    #[default]
13    Closed,
14    /// Market is in call phase
15    Call,
16    /// Market is open for dealing
17    Deal,
18    /// Market is open for editing orders
19    Edit,
20    /// Market is open for closing positions only
21    ClosingOnly,
22    /// Market is open for dealing but not editing
23    DealNoEdit,
24    /// Market is in auction phase
25    Auction,
26    /// Market is in auction phase without editing
27    AuctionNoEdit,
28    /// Market trading is suspended
29    Suspend,
30}
31
32/// Structure for price data received from the IG Markets API
33/// Contains information about market prices and related data
34#[derive(DebugPretty, Clone, DisplaySimple, Serialize, Deserialize, Default)]
35pub struct PriceData {
36    /// Name of the item (usually the market ID)
37    pub item_name: String,
38    /// Position of the item in the subscription
39    pub item_pos: i32,
40    /// All price fields for this market
41    pub fields: PriceFields,
42    /// Fields that have changed in this update
43    pub changed_fields: PriceFields,
44    /// Whether this is a snapshot or an update
45    pub is_snapshot: bool,
46}
47
48/// Price field data containing bid, offer, and market status information
49#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
50pub struct PriceFields {
51    #[serde(rename = "MID_OPEN")]
52    #[serde(with = "string_as_float_opt")]
53    #[serde(skip_serializing_if = "Option::is_none")]
54    mid_open: Option<f64>,
55
56    #[serde(rename = "HIGH")]
57    #[serde(with = "string_as_float_opt")]
58    #[serde(skip_serializing_if = "Option::is_none")]
59    high: Option<f64>,
60
61    #[serde(rename = "LOW")]
62    #[serde(with = "string_as_float_opt")]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    low: Option<f64>,
65
66    #[serde(rename = "BIDQUOTEID")]
67    #[serde(skip_serializing_if = "Option::is_none")]
68    bid_quote_id: Option<String>,
69
70    #[serde(rename = "ASKQUOTEID")]
71    #[serde(skip_serializing_if = "Option::is_none")]
72    ask_quote_id: Option<String>,
73
74    // Bid ladder prices
75    #[serde(rename = "BIDPRICE1")]
76    #[serde(with = "string_as_float_opt")]
77    #[serde(skip_serializing_if = "Option::is_none")]
78    bid_price1: Option<f64>,
79
80    #[serde(rename = "BIDPRICE2")]
81    #[serde(with = "string_as_float_opt")]
82    #[serde(skip_serializing_if = "Option::is_none")]
83    bid_price2: Option<f64>,
84
85    #[serde(rename = "BIDPRICE3")]
86    #[serde(with = "string_as_float_opt")]
87    #[serde(skip_serializing_if = "Option::is_none")]
88    bid_price3: Option<f64>,
89
90    #[serde(rename = "BIDPRICE4")]
91    #[serde(with = "string_as_float_opt")]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    bid_price4: Option<f64>,
94
95    #[serde(rename = "BIDPRICE5")]
96    #[serde(with = "string_as_float_opt")]
97    #[serde(skip_serializing_if = "Option::is_none")]
98    bid_price5: Option<f64>,
99
100    // Ask ladder prices
101    #[serde(rename = "ASKPRICE1")]
102    #[serde(with = "string_as_float_opt")]
103    #[serde(skip_serializing_if = "Option::is_none")]
104    ask_price1: Option<f64>,
105
106    #[serde(rename = "ASKPRICE2")]
107    #[serde(with = "string_as_float_opt")]
108    #[serde(skip_serializing_if = "Option::is_none")]
109    ask_price2: Option<f64>,
110
111    #[serde(rename = "ASKPRICE3")]
112    #[serde(with = "string_as_float_opt")]
113    #[serde(skip_serializing_if = "Option::is_none")]
114    ask_price3: Option<f64>,
115
116    #[serde(rename = "ASKPRICE4")]
117    #[serde(with = "string_as_float_opt")]
118    #[serde(skip_serializing_if = "Option::is_none")]
119    ask_price4: Option<f64>,
120
121    #[serde(rename = "ASKPRICE5")]
122    #[serde(with = "string_as_float_opt")]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    ask_price5: Option<f64>,
125
126    // Bid sizes
127    #[serde(rename = "BIDSIZE1")]
128    #[serde(with = "string_as_float_opt")]
129    #[serde(skip_serializing_if = "Option::is_none")]
130    bid_size1: Option<f64>,
131
132    #[serde(rename = "BIDSIZE2")]
133    #[serde(with = "string_as_float_opt")]
134    #[serde(skip_serializing_if = "Option::is_none")]
135    bid_size2: Option<f64>,
136
137    #[serde(rename = "BIDSIZE3")]
138    #[serde(with = "string_as_float_opt")]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    bid_size3: Option<f64>,
141
142    #[serde(rename = "BIDSIZE4")]
143    #[serde(with = "string_as_float_opt")]
144    #[serde(skip_serializing_if = "Option::is_none")]
145    bid_size4: Option<f64>,
146
147    #[serde(rename = "BIDSIZE5")]
148    #[serde(with = "string_as_float_opt")]
149    #[serde(skip_serializing_if = "Option::is_none")]
150    bid_size5: Option<f64>,
151
152    // Ask sizes
153    #[serde(rename = "ASKSIZE1")]
154    #[serde(with = "string_as_float_opt")]
155    #[serde(skip_serializing_if = "Option::is_none")]
156    ask_size1: Option<f64>,
157
158    #[serde(rename = "ASKSIZE2")]
159    #[serde(with = "string_as_float_opt")]
160    #[serde(skip_serializing_if = "Option::is_none")]
161    ask_size2: Option<f64>,
162
163    #[serde(rename = "ASKSIZE3")]
164    #[serde(with = "string_as_float_opt")]
165    #[serde(skip_serializing_if = "Option::is_none")]
166    ask_size3: Option<f64>,
167
168    #[serde(rename = "ASKSIZE4")]
169    #[serde(with = "string_as_float_opt")]
170    #[serde(skip_serializing_if = "Option::is_none")]
171    ask_size4: Option<f64>,
172
173    #[serde(rename = "ASKSIZE5")]
174    #[serde(with = "string_as_float_opt")]
175    #[serde(skip_serializing_if = "Option::is_none")]
176    ask_size5: Option<f64>,
177
178    // Currencies
179    #[serde(rename = "CURRENCY0")]
180    #[serde(skip_serializing_if = "Option::is_none")]
181    currency0: Option<String>,
182
183    #[serde(rename = "CURRENCY1")]
184    #[serde(skip_serializing_if = "Option::is_none")]
185    currency1: Option<String>,
186
187    #[serde(rename = "CURRENCY2")]
188    #[serde(skip_serializing_if = "Option::is_none")]
189    currency2: Option<String>,
190
191    #[serde(rename = "CURRENCY3")]
192    #[serde(skip_serializing_if = "Option::is_none")]
193    currency3: Option<String>,
194
195    #[serde(rename = "CURRENCY4")]
196    #[serde(skip_serializing_if = "Option::is_none")]
197    currency4: Option<String>,
198
199    #[serde(rename = "CURRENCY5")]
200    #[serde(skip_serializing_if = "Option::is_none")]
201    currency5: Option<String>,
202
203    // Bid size thresholds
204    #[serde(rename = "C1BIDSIZE1-5")]
205    #[serde(with = "string_as_float_opt")]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    c1_bid_size: Option<f64>,
208
209    #[serde(rename = "C2BIDSIZE1-5")]
210    #[serde(with = "string_as_float_opt")]
211    #[serde(skip_serializing_if = "Option::is_none")]
212    c2_bid_size: Option<f64>,
213
214    #[serde(rename = "C3BIDSIZE1-5")]
215    #[serde(with = "string_as_float_opt")]
216    #[serde(skip_serializing_if = "Option::is_none")]
217    c3_bid_size: Option<f64>,
218
219    #[serde(rename = "C4BIDSIZE1-5")]
220    #[serde(with = "string_as_float_opt")]
221    #[serde(skip_serializing_if = "Option::is_none")]
222    c4_bid_size: Option<f64>,
223
224    #[serde(rename = "C5BIDSIZE1-5")]
225    #[serde(with = "string_as_float_opt")]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    c5_bid_size: Option<f64>,
228
229    // Ask size thresholds
230    #[serde(rename = "C1ASKSIZE1-5")]
231    #[serde(with = "string_as_float_opt")]
232    #[serde(skip_serializing_if = "Option::is_none")]
233    c1_ask_size: Option<f64>,
234
235    #[serde(rename = "C2ASKSIZE1-5")]
236    #[serde(with = "string_as_float_opt")]
237    #[serde(skip_serializing_if = "Option::is_none")]
238    c2_ask_size: Option<f64>,
239
240    #[serde(rename = "C3ASKSIZE1-5")]
241    #[serde(with = "string_as_float_opt")]
242    #[serde(skip_serializing_if = "Option::is_none")]
243    c3_ask_size: Option<f64>,
244
245    #[serde(rename = "C4ASKSIZE1-5")]
246    #[serde(with = "string_as_float_opt")]
247    #[serde(skip_serializing_if = "Option::is_none")]
248    c4_ask_size: Option<f64>,
249
250    #[serde(rename = "C5ASKSIZE1-5")]
251    #[serde(with = "string_as_float_opt")]
252    #[serde(skip_serializing_if = "Option::is_none")]
253    c5_ask_size: Option<f64>,
254
255    #[serde(rename = "TIMESTAMP")]
256    #[serde(with = "string_as_float_opt")]
257    #[serde(skip_serializing_if = "Option::is_none")]
258    timestamp: Option<f64>,
259
260    #[serde(rename = "DLG_FLAG")]
261    #[serde(skip_serializing_if = "Option::is_none")]
262    dealing_flag: Option<DealingFlag>,
263}
264
265impl PriceData {
266    /// Converts a Lightstreamer ItemUpdate to a PriceData object
267    ///
268    /// # Arguments
269    ///
270    /// * `item_update` - The ItemUpdate from Lightstreamer containing price data
271    ///
272    /// # Returns
273    ///
274    /// A Result containing either the parsed PriceData or an error message
275    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
276        // Extract the item_name, defaulting to an empty string if None
277        let item_name = item_update.item_name.clone().unwrap_or_default();
278
279        // Convert item_pos from usize to i32
280        let item_pos = item_update.item_pos as i32;
281
282        // Extract is_snapshot
283        let is_snapshot = item_update.is_snapshot;
284
285        // Convert fields
286        let fields = Self::create_price_fields(&item_update.fields)?;
287
288        // Convert changed_fields by first creating a HashMap<String, Option<String>>
289        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
290        for (key, value) in &item_update.changed_fields {
291            changed_fields_map.insert(key.clone(), Some(value.clone()));
292        }
293        let changed_fields = Self::create_price_fields(&changed_fields_map)?;
294
295        Ok(PriceData {
296            item_name,
297            item_pos,
298            fields,
299            changed_fields,
300            is_snapshot,
301        })
302    }
303
304    // Helper method to create PriceFields from a HashMap
305    fn create_price_fields(
306        fields_map: &HashMap<String, Option<String>>,
307    ) -> Result<PriceFields, String> {
308        // Helper function to safely get a field value
309        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
310
311        // Helper function to parse float values
312        let parse_float = |key: &str| -> Result<Option<f64>, String> {
313            match get_field(key) {
314                Some(val) if !val.is_empty() => val
315                    .parse::<f64>()
316                    .map(Some)
317                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
318                _ => Ok(None),
319            }
320        };
321
322        // Parse dealing flag
323        let dealing_flag = match get_field("DLG_FLAG").as_deref() {
324            Some("CLOSED") => Some(DealingFlag::Closed),
325            Some("CALL") => Some(DealingFlag::Call),
326            Some("DEAL") => Some(DealingFlag::Deal),
327            Some("EDIT") => Some(DealingFlag::Edit),
328            Some("CLOSINGONLY") => Some(DealingFlag::ClosingOnly),
329            Some("DEALNOEDIT") => Some(DealingFlag::DealNoEdit),
330            Some("AUCTION") => Some(DealingFlag::Auction),
331            Some("AUCTIONNOEDIT") => Some(DealingFlag::AuctionNoEdit),
332            Some("SUSPEND") => Some(DealingFlag::Suspend),
333            Some(unknown) => return Err(format!("Unknown dealing flag: {unknown}")),
334            None => None,
335        };
336
337        Ok(PriceFields {
338            mid_open: parse_float("MID_OPEN")?,
339            high: parse_float("HIGH")?,
340            low: parse_float("LOW")?,
341            bid_quote_id: get_field("BIDQUOTEID"),
342            ask_quote_id: get_field("ASKQUOTEID"),
343
344            // Bid ladder prices
345            bid_price1: parse_float("BIDPRICE1")?,
346            bid_price2: parse_float("BIDPRICE2")?,
347            bid_price3: parse_float("BIDPRICE3")?,
348            bid_price4: parse_float("BIDPRICE4")?,
349            bid_price5: parse_float("BIDPRICE5")?,
350
351            // Ask ladder prices
352            ask_price1: parse_float("ASKPRICE1")?,
353            ask_price2: parse_float("ASKPRICE2")?,
354            ask_price3: parse_float("ASKPRICE3")?,
355            ask_price4: parse_float("ASKPRICE4")?,
356            ask_price5: parse_float("ASKPRICE5")?,
357
358            // Bid sizes
359            bid_size1: parse_float("BIDSIZE1")?,
360            bid_size2: parse_float("BIDSIZE2")?,
361            bid_size3: parse_float("BIDSIZE3")?,
362            bid_size4: parse_float("BIDSIZE4")?,
363            bid_size5: parse_float("BIDSIZE5")?,
364
365            // Ask sizes
366            ask_size1: parse_float("ASKSIZE1")?,
367            ask_size2: parse_float("ASKSIZE2")?,
368            ask_size3: parse_float("ASKSIZE3")?,
369            ask_size4: parse_float("ASKSIZE4")?,
370            ask_size5: parse_float("ASKSIZE5")?,
371
372            // Currencies
373            currency0: get_field("CURRENCY0"),
374            currency1: get_field("CURRENCY1"),
375            currency2: get_field("CURRENCY2"),
376            currency3: get_field("CURRENCY3"),
377            currency4: get_field("CURRENCY4"),
378            currency5: get_field("CURRENCY5"),
379
380            // Bid size thresholds
381            c1_bid_size: parse_float("C1BIDSIZE1-5")?,
382            c2_bid_size: parse_float("C2BIDSIZE1-5")?,
383            c3_bid_size: parse_float("C3BIDSIZE1-5")?,
384            c4_bid_size: parse_float("C4BIDSIZE1-5")?,
385            c5_bid_size: parse_float("C5BIDSIZE1-5")?,
386
387            // Ask size thresholds
388            c1_ask_size: parse_float("C1ASKSIZE1-5")?,
389            c2_ask_size: parse_float("C2ASKSIZE1-5")?,
390            c3_ask_size: parse_float("C3ASKSIZE1-5")?,
391            c4_ask_size: parse_float("C4ASKSIZE1-5")?,
392            c5_ask_size: parse_float("C5ASKSIZE1-5")?,
393
394            timestamp: parse_float("TIMESTAMP")?,
395            dealing_flag,
396        })
397    }
398}
399
400impl From<&ItemUpdate> for PriceData {
401    fn from(item_update: &ItemUpdate) -> Self {
402        PriceData::from_item_update(item_update).unwrap_or_default()
403    }
404}