Skip to main content

ig_client/presentation/
price.rs

1use crate::presentation::serialization::{
2    string_as_bool_opt, string_as_float_opt, string_as_int_opt,
3};
4use pretty_simple_display::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Market dealing status flags indicating trading availability
9#[repr(u8)]
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum DealingFlag {
13    /// Market is closed for trading
14    #[default]
15    Closed,
16    /// Market is in call phase
17    Call,
18    /// Market is open for dealing
19    Deal,
20    /// Market is open for editing orders
21    Edit,
22    /// Market is open for closing positions only
23    ClosingsOnly,
24    /// Market is open for dealing but not editing
25    DealNoEdit,
26    /// Market is in auction phase
27    Auction,
28    /// Market is in auction phase without editing
29    AuctionNoEdit,
30    /// Market trading is suspended
31    Suspend,
32}
33
34/// Structure for price data received from the IG Markets API
35/// Contains information about market prices and related data
36#[derive(DebugPretty, Clone, DisplaySimple, Serialize, Deserialize, Default)]
37pub struct PriceData {
38    /// Name of the item (usually the market ID)
39    pub item_name: String,
40    /// Position of the item in the subscription
41    pub item_pos: usize,
42    /// All price fields for this market
43    pub fields: PriceFields,
44    /// Fields that have changed in this update
45    pub changed_fields: PriceFields,
46    /// Whether this is a snapshot or an update
47    pub is_snapshot: bool,
48}
49
50/// Price field data containing bid, offer, and market status information
51///
52/// Every field carries `skip_serializing_if = "Option::is_none"`, so absent
53/// values are omitted from the serialized output. The struct-level
54/// `#[serde(default)]` makes the reverse direction symmetric: a missing field
55/// deserializes back to `None` instead of failing, so the DTO round-trips its
56/// own output.
57#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
58#[serde(default)]
59pub struct PriceFields {
60    /// The opening price at the middle of the bid-ask spread
61    #[serde(rename = "MID_OPEN")]
62    #[serde(with = "string_as_float_opt")]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub mid_open: Option<f64>,
65
66    /// The highest price reached during the trading session
67    #[serde(rename = "HIGH")]
68    #[serde(with = "string_as_float_opt")]
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub high: Option<f64>,
71
72    /// The lowest price reached during the trading session
73    #[serde(rename = "LOW")]
74    #[serde(with = "string_as_float_opt")]
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub low: Option<f64>,
77
78    /// Current bid price for the instrument
79    #[serde(rename = "BID")]
80    #[serde(with = "string_as_float_opt")]
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub bid: Option<f64>,
83
84    /// Current offer (ask) price for the instrument
85    #[serde(rename = "OFFER")]
86    #[serde(with = "string_as_float_opt")]
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub offer: Option<f64>,
89
90    /// Price change from previous close
91    #[serde(rename = "CHANGE")]
92    #[serde(with = "string_as_float_opt")]
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub change: Option<f64>,
95
96    /// Percentage change from previous close
97    #[serde(rename = "CHANGE_PCT")]
98    #[serde(with = "string_as_float_opt")]
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub change_pct: Option<f64>,
101
102    /// Delayed-data flag (1 = delayed).
103    ///
104    /// This is IG's `MARKET_DELAY` wire field: a 0/1 flag, not a duration.
105    /// Kept in sync with [`crate::presentation::market::MarketFields::market_delay`],
106    /// which parses the same field the same way.
107    #[serde(rename = "MARKET_DELAY")]
108    #[serde(with = "string_as_bool_opt")]
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub market_delay: Option<bool>,
111
112    /// Current market state (e.g., "OPEN", "CLOSED", "SUSPENDED")
113    #[serde(rename = "MARKET_STATE")]
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub market_state: Option<String>,
116
117    /// Timestamp of the last price update
118    #[serde(rename = "UPDATE_TIME")]
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub update_time: Option<String>,
121
122    /// Unique identifier for the bid quote
123    #[serde(rename = "BIDQUOTEID")]
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub bid_quote_id: Option<String>,
126
127    /// Unique identifier for the ask quote
128    #[serde(rename = "ASKQUOTEID")]
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub ask_quote_id: Option<String>,
131
132    // Bid ladder prices
133    /// First level bid price in the order book
134    #[serde(rename = "BIDPRICE1")]
135    #[serde(with = "string_as_float_opt")]
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub bid_price1: Option<f64>,
138
139    /// Second level bid price in the order book
140    #[serde(rename = "BIDPRICE2")]
141    #[serde(with = "string_as_float_opt")]
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub bid_price2: Option<f64>,
144
145    /// Third level bid price in the order book
146    #[serde(rename = "BIDPRICE3")]
147    #[serde(with = "string_as_float_opt")]
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub bid_price3: Option<f64>,
150
151    /// Fourth level bid price in the order book
152    #[serde(rename = "BIDPRICE4")]
153    #[serde(with = "string_as_float_opt")]
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub bid_price4: Option<f64>,
156
157    /// Fifth level bid price in the order book
158    #[serde(rename = "BIDPRICE5")]
159    #[serde(with = "string_as_float_opt")]
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub bid_price5: Option<f64>,
162
163    // Ask ladder prices
164    /// First level ask price in the order book
165    #[serde(rename = "ASKPRICE1")]
166    #[serde(with = "string_as_float_opt")]
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub ask_price1: Option<f64>,
169
170    /// Second level ask price in the order book
171    #[serde(rename = "ASKPRICE2")]
172    #[serde(with = "string_as_float_opt")]
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub ask_price2: Option<f64>,
175
176    /// Third level ask price in the order book
177    #[serde(rename = "ASKPRICE3")]
178    #[serde(with = "string_as_float_opt")]
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub ask_price3: Option<f64>,
181
182    /// Fourth level ask price in the order book
183    #[serde(rename = "ASKPRICE4")]
184    #[serde(with = "string_as_float_opt")]
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub ask_price4: Option<f64>,
187
188    /// Fifth level ask price in the order book
189    #[serde(rename = "ASKPRICE5")]
190    #[serde(with = "string_as_float_opt")]
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub ask_price5: Option<f64>,
193
194    // Bid sizes
195    /// Volume available at the first level bid price
196    #[serde(rename = "BIDSIZE1")]
197    #[serde(with = "string_as_float_opt")]
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub bid_size1: Option<f64>,
200
201    /// Volume available at the second level bid price
202    #[serde(rename = "BIDSIZE2")]
203    #[serde(with = "string_as_float_opt")]
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub bid_size2: Option<f64>,
206
207    /// Volume available at the third level bid price
208    #[serde(rename = "BIDSIZE3")]
209    #[serde(with = "string_as_float_opt")]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub bid_size3: Option<f64>,
212
213    /// Volume available at the fourth level bid price
214    #[serde(rename = "BIDSIZE4")]
215    #[serde(with = "string_as_float_opt")]
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub bid_size4: Option<f64>,
218
219    /// Volume available at the fifth level bid price
220    #[serde(rename = "BIDSIZE5")]
221    #[serde(with = "string_as_float_opt")]
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub bid_size5: Option<f64>,
224
225    // Ask sizes
226    /// Volume available at the first level ask price
227    #[serde(rename = "ASKSIZE1")]
228    #[serde(with = "string_as_float_opt")]
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub ask_size1: Option<f64>,
231
232    /// Volume available at the second level ask price
233    #[serde(rename = "ASKSIZE2")]
234    #[serde(with = "string_as_float_opt")]
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub ask_size2: Option<f64>,
237
238    /// Volume available at the third level ask price
239    #[serde(rename = "ASKSIZE3")]
240    #[serde(with = "string_as_float_opt")]
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub ask_size3: Option<f64>,
243
244    /// Volume available at the fourth level ask price
245    #[serde(rename = "ASKSIZE4")]
246    #[serde(with = "string_as_float_opt")]
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub ask_size4: Option<f64>,
249
250    /// Volume available at the fifth level ask price
251    #[serde(rename = "ASKSIZE5")]
252    #[serde(with = "string_as_float_opt")]
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub ask_size5: Option<f64>,
255
256    /// Base currency code for the trading pair
257    #[serde(rename = "CURRENCY0")]
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub currency0: Option<String>,
260
261    /// First alternative currency code
262    #[serde(rename = "CURRENCY1")]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub currency1: Option<String>,
265
266    /// Second alternative currency code
267    #[serde(rename = "CURRENCY2")]
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub currency2: Option<String>,
270
271    /// Third alternative currency code
272    #[serde(rename = "CURRENCY3")]
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub currency3: Option<String>,
275
276    /// Fourth alternative currency code
277    #[serde(rename = "CURRENCY4")]
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub currency4: Option<String>,
280
281    /// Fifth alternative currency code
282    #[serde(rename = "CURRENCY5")]
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub currency5: Option<String>,
285
286    /// Bid size for currency 1 at level 1
287    #[serde(rename = "C1BIDSIZE1")]
288    #[serde(with = "string_as_float_opt")]
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub c1_bid_size_1: Option<f64>,
291
292    /// Bid size for currency 1 at level 2
293    #[serde(rename = "C1BIDSIZE2")]
294    #[serde(with = "string_as_float_opt")]
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub c1_bid_size_2: Option<f64>,
297
298    /// Bid size for currency 1 at level 3
299    #[serde(rename = "C1BIDSIZE3")]
300    #[serde(with = "string_as_float_opt")]
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub c1_bid_size_3: Option<f64>,
303
304    /// Bid size for currency 1 at level 4
305    #[serde(rename = "C1BIDSIZE4")]
306    #[serde(with = "string_as_float_opt")]
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub c1_bid_size_4: Option<f64>,
309
310    /// Bid size for currency 1 at level 5
311    #[serde(rename = "C1BIDSIZE5")]
312    #[serde(with = "string_as_float_opt")]
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub c1_bid_size_5: Option<f64>,
315
316    /// Bid size for currency 2 at level 1
317    #[serde(rename = "C2BIDSIZE1")]
318    #[serde(with = "string_as_float_opt")]
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub c2_bid_size_1: Option<f64>,
321
322    /// Bid size for currency 2 at level 2
323    #[serde(rename = "C2BIDSIZE2")]
324    #[serde(with = "string_as_float_opt")]
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub c2_bid_size_2: Option<f64>,
327
328    /// Bid size for currency 2 at level 3
329    #[serde(rename = "C2BIDSIZE3")]
330    #[serde(with = "string_as_float_opt")]
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub c2_bid_size_3: Option<f64>,
333
334    /// Bid size for currency 2 at level 4
335    #[serde(rename = "C2BIDSIZE4")]
336    #[serde(with = "string_as_float_opt")]
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub c2_bid_size_4: Option<f64>,
339
340    /// Bid size for currency 2 at level 5
341    #[serde(rename = "C2BIDSIZE5")]
342    #[serde(with = "string_as_float_opt")]
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub c2_bid_size_5: Option<f64>,
345
346    /// Bid size for currency 3 at level 1
347    #[serde(rename = "C3BIDSIZE1")]
348    #[serde(with = "string_as_float_opt")]
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub c3_bid_size_1: Option<f64>,
351
352    /// Bid size for currency 3 at level 2
353    #[serde(rename = "C3BIDSIZE2")]
354    #[serde(with = "string_as_float_opt")]
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub c3_bid_size_2: Option<f64>,
357
358    /// Bid size for currency 3 at level 3
359    #[serde(rename = "C3BIDSIZE3")]
360    #[serde(with = "string_as_float_opt")]
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub c3_bid_size_3: Option<f64>,
363
364    /// Bid size for currency 3 at level 4
365    #[serde(rename = "C3BIDSIZE4")]
366    #[serde(with = "string_as_float_opt")]
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub c3_bid_size_4: Option<f64>,
369
370    /// Bid size for currency 3 at level 5
371    #[serde(rename = "C3BIDSIZE5")]
372    #[serde(with = "string_as_float_opt")]
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub c3_bid_size_5: Option<f64>,
375
376    /// Bid size for currency 4 at level 1
377    #[serde(rename = "C4BIDSIZE1")]
378    #[serde(with = "string_as_float_opt")]
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub c4_bid_size_1: Option<f64>,
381
382    /// Bid size for currency 4 at level 2
383    #[serde(rename = "C4BIDSIZE2")]
384    #[serde(with = "string_as_float_opt")]
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub c4_bid_size_2: Option<f64>,
387
388    /// Bid size for currency 4 at level 3
389    #[serde(rename = "C4BIDSIZE3")]
390    #[serde(with = "string_as_float_opt")]
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub c4_bid_size_3: Option<f64>,
393
394    /// Bid size for currency 4 at level 4
395    #[serde(rename = "C4BIDSIZE4")]
396    #[serde(with = "string_as_float_opt")]
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub c4_bid_size_4: Option<f64>,
399
400    /// Bid size for currency 4 at level 5
401    #[serde(rename = "C4BIDSIZE5")]
402    #[serde(with = "string_as_float_opt")]
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub c4_bid_size_5: Option<f64>,
405
406    /// Bid size for currency 5 at level 1
407    #[serde(rename = "C5BIDSIZE1")]
408    #[serde(with = "string_as_float_opt")]
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub c5_bid_size_1: Option<f64>,
411
412    /// Bid size for currency 5 at level 2
413    #[serde(rename = "C5BIDSIZE2")]
414    #[serde(with = "string_as_float_opt")]
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub c5_bid_size_2: Option<f64>,
417
418    /// Bid size for currency 5 at level 3
419    #[serde(rename = "C5BIDSIZE3")]
420    #[serde(with = "string_as_float_opt")]
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub c5_bid_size_3: Option<f64>,
423
424    /// Bid size for currency 5 at level 4
425    #[serde(rename = "C5BIDSIZE4")]
426    #[serde(with = "string_as_float_opt")]
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub c5_bid_size_4: Option<f64>,
429
430    /// Bid size for currency 5 at level 5
431    #[serde(rename = "C5BIDSIZE5")]
432    #[serde(with = "string_as_float_opt")]
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub c5_bid_size_5: Option<f64>,
435
436    // Ask sizes for different currencies
437    /// Ask size for currency 1 at level 1
438    #[serde(rename = "C1ASKSIZE1")]
439    #[serde(with = "string_as_float_opt")]
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub c1_ask_size_1: Option<f64>,
442
443    /// Ask size for currency 1 at level 2
444    #[serde(rename = "C1ASKSIZE2")]
445    #[serde(with = "string_as_float_opt")]
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub c1_ask_size_2: Option<f64>,
448
449    /// Ask size for currency 1 at level 3
450    #[serde(rename = "C1ASKSIZE3")]
451    #[serde(with = "string_as_float_opt")]
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub c1_ask_size_3: Option<f64>,
454
455    /// Ask size for currency 1 at level 4
456    #[serde(rename = "C1ASKSIZE4")]
457    #[serde(with = "string_as_float_opt")]
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub c1_ask_size_4: Option<f64>,
460
461    /// Ask size for currency 1 at level 5
462    #[serde(rename = "C1ASKSIZE5")]
463    #[serde(with = "string_as_float_opt")]
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub c1_ask_size_5: Option<f64>,
466
467    /// Ask size for currency 2 at level 1
468    #[serde(rename = "C2ASKSIZE1")]
469    #[serde(with = "string_as_float_opt")]
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub c2_ask_size_1: Option<f64>,
472
473    /// Ask size for currency 2 at level 2
474    #[serde(rename = "C2ASKSIZE2")]
475    #[serde(with = "string_as_float_opt")]
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub c2_ask_size_2: Option<f64>,
478
479    /// Ask size for currency 2 at level 3
480    #[serde(rename = "C2ASKSIZE3")]
481    #[serde(with = "string_as_float_opt")]
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub c2_ask_size_3: Option<f64>,
484
485    /// Ask size for currency 2 at level 4
486    #[serde(rename = "C2ASKSIZE4")]
487    #[serde(with = "string_as_float_opt")]
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub c2_ask_size_4: Option<f64>,
490
491    /// Ask size for currency 2 at level 5
492    #[serde(rename = "C2ASKSIZE5")]
493    #[serde(with = "string_as_float_opt")]
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub c2_ask_size_5: Option<f64>,
496
497    /// Ask size for currency 3 at level 1
498    #[serde(rename = "C3ASKSIZE1")]
499    #[serde(with = "string_as_float_opt")]
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub c3_ask_size_1: Option<f64>,
502
503    /// Ask size for currency 3 at level 2
504    #[serde(rename = "C3ASKSIZE2")]
505    #[serde(with = "string_as_float_opt")]
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub c3_ask_size_2: Option<f64>,
508
509    /// Ask size for currency 3 at level 3
510    #[serde(rename = "C3ASKSIZE3")]
511    #[serde(with = "string_as_float_opt")]
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub c3_ask_size_3: Option<f64>,
514
515    /// Ask size for currency 3 at level 4
516    #[serde(rename = "C3ASKSIZE4")]
517    #[serde(with = "string_as_float_opt")]
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub c3_ask_size_4: Option<f64>,
520
521    /// Ask size for currency 3 at level 5
522    #[serde(rename = "C3ASKSIZE5")]
523    #[serde(with = "string_as_float_opt")]
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub c3_ask_size_5: Option<f64>,
526
527    /// Ask size for currency 4 at level 1
528    #[serde(rename = "C4ASKSIZE1")]
529    #[serde(with = "string_as_float_opt")]
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub c4_ask_size_1: Option<f64>,
532
533    /// Ask size for currency 4 at level 2
534    #[serde(rename = "C4ASKSIZE2")]
535    #[serde(with = "string_as_float_opt")]
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub c4_ask_size_2: Option<f64>,
538
539    /// Ask size for currency 4 at level 3
540    #[serde(rename = "C4ASKSIZE3")]
541    #[serde(with = "string_as_float_opt")]
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub c4_ask_size_3: Option<f64>,
544
545    /// Ask size for currency 4 at level 4
546    #[serde(rename = "C4ASKSIZE4")]
547    #[serde(with = "string_as_float_opt")]
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub c4_ask_size_4: Option<f64>,
550
551    /// Ask size for currency 4 at level 5
552    #[serde(rename = "C4ASKSIZE5")]
553    #[serde(with = "string_as_float_opt")]
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub c4_ask_size_5: Option<f64>,
556
557    /// Ask size for currency 5 at level 1
558    #[serde(rename = "C5ASKSIZE1")]
559    #[serde(with = "string_as_float_opt")]
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub c5_ask_size_1: Option<f64>,
562
563    /// Ask size for currency 5 at level 2
564    #[serde(rename = "C5ASKSIZE2")]
565    #[serde(with = "string_as_float_opt")]
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub c5_ask_size_2: Option<f64>,
568
569    /// Ask size for currency 5 at level 3
570    #[serde(rename = "C5ASKSIZE3")]
571    #[serde(with = "string_as_float_opt")]
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub c5_ask_size_3: Option<f64>,
574
575    /// Ask size for currency 5 at level 4
576    #[serde(rename = "C5ASKSIZE4")]
577    #[serde(with = "string_as_float_opt")]
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub c5_ask_size_4: Option<f64>,
580
581    /// Ask size for currency 5 at level 5
582    #[serde(rename = "C5ASKSIZE5")]
583    #[serde(with = "string_as_float_opt")]
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub c5_ask_size_5: Option<f64>,
586
587    /// The timestamp of the price update, in epoch milliseconds (UTC).
588    ///
589    /// Typed as `i64` to preserve integer epoch-millis exactly (float parsing
590    /// would risk silent rounding on large values).
591    #[serde(rename = "TIMESTAMP")]
592    #[serde(with = "string_as_int_opt")]
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub timestamp: Option<i64>,
595
596    /// Dealing status flag indicating trading availability/state of the market
597    #[serde(rename = "DLG_FLAG")]
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub dealing_flag: Option<DealingFlag>,
600
601    /// Price change versus open
602    #[serde(rename = "NET_CHG")]
603    #[serde(with = "string_as_float_opt")]
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub net_chg: Option<f64>,
606
607    /// Percentage change versus open
608    #[serde(rename = "NET_CHG_PCT")]
609    #[serde(with = "string_as_float_opt")]
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub net_chg_pct: Option<f64>,
612
613    /// Delayed price flag (0 = false, 1 = true)
614    #[serde(rename = "DELAY")]
615    #[serde(with = "string_as_float_opt")]
616    #[serde(skip_serializing_if = "Option::is_none")]
617    pub delay: Option<f64>,
618}
619
620impl PriceData {
621    /// Builds a [`PriceData`] from pre-extracted streaming fields.
622    ///
623    /// This is transport-agnostic: it takes plain field maps rather than a
624    /// Lightstreamer `ItemUpdate`, so the presentation layer carries no
625    /// dependency on the streaming transport. The `ItemUpdate` adapter lives in
626    /// `application::streaming_convert` (feature `streaming`).
627    ///
628    /// # Arguments
629    /// * `item_name` - Subscription item name (`None` when subscribed by position)
630    /// * `item_pos` - 1-based position of the item in the subscription
631    /// * `is_snapshot` - Whether this update is a snapshot
632    /// * `fields` - Current field values for the item
633    /// * `changed_fields` - Field values that changed in this update
634    ///
635    /// # Returns
636    /// A Result containing either the parsed PriceData or an error message
637    pub fn from_fields(
638        item_name: Option<&str>,
639        item_pos: usize,
640        is_snapshot: bool,
641        fields: &HashMap<String, Option<String>>,
642        changed_fields: &HashMap<String, Option<String>>,
643    ) -> Result<Self, String> {
644        let fields = Self::create_price_fields(fields)?;
645        let changed_fields = Self::create_price_fields(changed_fields)?;
646
647        Ok(PriceData {
648            item_name: item_name.unwrap_or_default().to_string(),
649            item_pos,
650            fields,
651            changed_fields,
652            is_snapshot,
653        })
654    }
655
656    // Helper method to create PriceFields from a HashMap
657    fn create_price_fields(
658        fields_map: &HashMap<String, Option<String>>,
659    ) -> Result<PriceFields, String> {
660        // Helper function to safely get a field value
661        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
662
663        // Helper function to parse float values
664        let parse_float = |key: &str| -> Result<Option<f64>, String> {
665            match get_field(key) {
666                Some(val) if !val.is_empty() => val
667                    .parse::<f64>()
668                    .map(Some)
669                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
670                _ => Ok(None),
671            }
672        };
673
674        // Helper function to parse integer values (e.g. epoch-millis timestamps).
675        let parse_int = |key: &str| -> Result<Option<i64>, String> {
676            match get_field(key) {
677                Some(val) if !val.is_empty() => val
678                    .parse::<i64>()
679                    .map(Some)
680                    .map_err(|_| format!("Failed to parse {key} as integer: {val}")),
681                _ => Ok(None),
682            }
683        };
684
685        // Parse the MARKET_DELAY flag: IG sends "0" / "1" (delayed-data flag),
686        // not a duration. An empty string is treated as None.
687        let market_delay = match get_field("MARKET_DELAY").as_deref() {
688            Some("0") => Some(false),
689            Some("1") => Some(true),
690            Some("") | None => None,
691            Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
692        };
693
694        // Parse dealing flag (case-insensitive to handle potential lowercase conversion).
695        // An empty string (produced when the server sends '#' for null) is treated as None.
696        let dealing_flag = match get_field("DLG_FLAG")
697            .as_deref()
698            .map(str::trim)
699            .filter(|s| !s.is_empty())
700            .map(|s| s.to_uppercase())
701            .as_deref()
702        {
703            Some("CLOSED") => Some(DealingFlag::Closed),
704            Some("CALL") => Some(DealingFlag::Call),
705            Some("DEAL") => Some(DealingFlag::Deal),
706            Some("EDIT") => Some(DealingFlag::Edit),
707            Some("CLOSINGSONLY") | Some("CLOSINGONLY") => Some(DealingFlag::ClosingsOnly),
708            Some("DEALNOEDIT") => Some(DealingFlag::DealNoEdit),
709            Some("AUCTION") => Some(DealingFlag::Auction),
710            Some("AUCTIONNOEDIT") => Some(DealingFlag::AuctionNoEdit),
711            Some("SUSPEND") => Some(DealingFlag::Suspend),
712            Some(unknown) => return Err(format!("Unknown dealing flag: {unknown}")),
713            None => None,
714        };
715
716        Ok(PriceFields {
717            mid_open: parse_float("MID_OPEN")?,
718            high: parse_float("HIGH")?,
719            low: parse_float("LOW")?,
720            bid: parse_float("BID")?,
721            offer: parse_float("OFFER")?,
722            change: parse_float("CHANGE")?,
723            change_pct: parse_float("CHANGE_PCT")?,
724            market_delay,
725            market_state: get_field("MARKET_STATE"),
726            update_time: get_field("UPDATE_TIME"),
727
728            bid_quote_id: get_field("BIDQUOTEID"),
729            ask_quote_id: get_field("ASKQUOTEID"),
730
731            // Bid ladder prices
732            bid_price1: parse_float("BIDPRICE1")?,
733            bid_price2: parse_float("BIDPRICE2")?,
734            bid_price3: parse_float("BIDPRICE3")?,
735            bid_price4: parse_float("BIDPRICE4")?,
736            bid_price5: parse_float("BIDPRICE5")?,
737
738            // Ask ladder prices
739            ask_price1: parse_float("ASKPRICE1")?,
740            ask_price2: parse_float("ASKPRICE2")?,
741            ask_price3: parse_float("ASKPRICE3")?,
742            ask_price4: parse_float("ASKPRICE4")?,
743            ask_price5: parse_float("ASKPRICE5")?,
744
745            // Bid sizes
746            bid_size1: parse_float("BIDSIZE1")?,
747            bid_size2: parse_float("BIDSIZE2")?,
748            bid_size3: parse_float("BIDSIZE3")?,
749            bid_size4: parse_float("BIDSIZE4")?,
750            bid_size5: parse_float("BIDSIZE5")?,
751
752            // Ask sizes
753            ask_size1: parse_float("ASKSIZE1")?,
754            ask_size2: parse_float("ASKSIZE2")?,
755            ask_size3: parse_float("ASKSIZE3")?,
756            ask_size4: parse_float("ASKSIZE4")?,
757            ask_size5: parse_float("ASKSIZE5")?,
758
759            // Currencies
760            currency0: get_field("CURRENCY0"),
761            currency1: get_field("CURRENCY1"),
762            currency2: get_field("CURRENCY2"),
763            currency3: get_field("CURRENCY3"),
764            currency4: get_field("CURRENCY4"),
765            currency5: get_field("CURRENCY5"),
766
767            // Bid size thresholds (expanded 1..5 for C1..C5)
768            c1_bid_size_1: parse_float("C1BIDSIZE1")?,
769            c1_bid_size_2: parse_float("C1BIDSIZE2")?,
770            c1_bid_size_3: parse_float("C1BIDSIZE3")?,
771            c1_bid_size_4: parse_float("C1BIDSIZE4")?,
772            c1_bid_size_5: parse_float("C1BIDSIZE5")?,
773
774            c2_bid_size_1: parse_float("C2BIDSIZE1")?,
775            c2_bid_size_2: parse_float("C2BIDSIZE2")?,
776            c2_bid_size_3: parse_float("C2BIDSIZE3")?,
777            c2_bid_size_4: parse_float("C2BIDSIZE4")?,
778            c2_bid_size_5: parse_float("C2BIDSIZE5")?,
779
780            c3_bid_size_1: parse_float("C3BIDSIZE1")?,
781            c3_bid_size_2: parse_float("C3BIDSIZE2")?,
782            c3_bid_size_3: parse_float("C3BIDSIZE3")?,
783            c3_bid_size_4: parse_float("C3BIDSIZE4")?,
784            c3_bid_size_5: parse_float("C3BIDSIZE5")?,
785
786            c4_bid_size_1: parse_float("C4BIDSIZE1")?,
787            c4_bid_size_2: parse_float("C4BIDSIZE2")?,
788            c4_bid_size_3: parse_float("C4BIDSIZE3")?,
789            c4_bid_size_4: parse_float("C4BIDSIZE4")?,
790            c4_bid_size_5: parse_float("C4BIDSIZE5")?,
791
792            c5_bid_size_1: parse_float("C5BIDSIZE1")?,
793            c5_bid_size_2: parse_float("C5BIDSIZE2")?,
794            c5_bid_size_3: parse_float("C5BIDSIZE3")?,
795            c5_bid_size_4: parse_float("C5BIDSIZE4")?,
796            c5_bid_size_5: parse_float("C5BIDSIZE5")?,
797
798            // Ask size thresholds (expanded 1..5 for C1..C5)
799            c1_ask_size_1: parse_float("C1ASKSIZE1")?,
800            c1_ask_size_2: parse_float("C1ASKSIZE2")?,
801            c1_ask_size_3: parse_float("C1ASKSIZE3")?,
802            c1_ask_size_4: parse_float("C1ASKSIZE4")?,
803            c1_ask_size_5: parse_float("C1ASKSIZE5")?,
804
805            c2_ask_size_1: parse_float("C2ASKSIZE1")?,
806            c2_ask_size_2: parse_float("C2ASKSIZE2")?,
807            c2_ask_size_3: parse_float("C2ASKSIZE3")?,
808            c2_ask_size_4: parse_float("C2ASKSIZE4")?,
809            c2_ask_size_5: parse_float("C2ASKSIZE5")?,
810
811            c3_ask_size_1: parse_float("C3ASKSIZE1")?,
812            c3_ask_size_2: parse_float("C3ASKSIZE2")?,
813            c3_ask_size_3: parse_float("C3ASKSIZE3")?,
814            c3_ask_size_4: parse_float("C3ASKSIZE4")?,
815            c3_ask_size_5: parse_float("C3ASKSIZE5")?,
816
817            c4_ask_size_1: parse_float("C4ASKSIZE1")?,
818            c4_ask_size_2: parse_float("C4ASKSIZE2")?,
819            c4_ask_size_3: parse_float("C4ASKSIZE3")?,
820            c4_ask_size_4: parse_float("C4ASKSIZE4")?,
821            c4_ask_size_5: parse_float("C4ASKSIZE5")?,
822
823            c5_ask_size_1: parse_float("C5ASKSIZE1")?,
824            c5_ask_size_2: parse_float("C5ASKSIZE2")?,
825            c5_ask_size_3: parse_float("C5ASKSIZE3")?,
826            c5_ask_size_4: parse_float("C5ASKSIZE4")?,
827            c5_ask_size_5: parse_float("C5ASKSIZE5")?,
828
829            timestamp: parse_int("TIMESTAMP")?,
830            dealing_flag,
831            net_chg: parse_float("NET_CHG")?,
832            net_chg_pct: parse_float("NET_CHG_PCT")?,
833            delay: parse_float("DELAY")?,
834        })
835    }
836}