ig_client/presentation/
price.rs

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