ig_client/presentation/
price.rs

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