ig_client/presentation/
market.rs

1use crate::presentation::serialization::{string_as_bool_opt, string_as_float_opt};
2use lightstreamer_rs::subscription::ItemUpdate;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
6
7/// Represents the current state of a market
8#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
9pub enum MarketState {
10    #[serde(rename = "closed")]
11    Closed,
12    #[serde(rename = "offline")]
13    #[default]
14    Offline,
15    #[serde(rename = "tradeable")]
16    Tradeable,
17    #[serde(rename = "edit")]
18    Edit,
19    #[serde(rename = "auction")]
20    Auction,
21    #[serde(rename = "auction_no_edit")]
22    AuctionNoEdit,
23    #[serde(rename = "suspended")]
24    Suspended,
25}
26
27/// Representation of market data received from the IG Markets streaming API
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct MarketData {
30    /// Name of the item this data belongs to
31    item_name: String,
32    /// Position of the item in the subscription
33    item_pos: i32,
34    /// All market fields
35    fields: MarketFields,
36    /// Fields that have changed in this update
37    changed_fields: MarketFields,
38    /// Whether this is a snapshot or an update
39    is_snapshot: bool,
40}
41
42impl MarketData {
43    /// Converts an ItemUpdate from the Lightstreamer API to a MarketData object
44    ///
45    /// # Arguments
46    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
47    ///
48    /// # Returns
49    /// * `Result<Self, String>` - The converted MarketData or an error message
50    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
51        // Extract the item_name, defaulting to an empty string if None
52        let item_name = item_update.item_name.clone().unwrap_or_default();
53
54        // Convert item_pos from usize to i32
55        let item_pos = item_update.item_pos as i32;
56
57        // Extract is_snapshot
58        let is_snapshot = item_update.is_snapshot;
59
60        // Convert fields
61        let fields = Self::create_market_fields(&item_update.fields)?;
62
63        // Convert changed_fields by first creating a HashMap<String, Option<String>>
64        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
65        for (key, value) in &item_update.changed_fields {
66            changed_fields_map.insert(key.clone(), Some(value.clone()));
67        }
68        let changed_fields = Self::create_market_fields(&changed_fields_map)?;
69
70        Ok(MarketData {
71            item_name,
72            item_pos,
73            fields,
74            changed_fields,
75            is_snapshot,
76        })
77    }
78
79    /// Helper method to create MarketFields from a HashMap of field values
80    ///
81    /// # Arguments
82    /// * `fields_map` - HashMap containing field names and their string values
83    ///
84    /// # Returns
85    /// * `Result<MarketFields, String>` - The parsed MarketFields or an error message
86    fn create_market_fields(
87        fields_map: &HashMap<String, Option<String>>,
88    ) -> Result<MarketFields, String> {
89        // Helper function to safely get a field value
90        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
91
92        // Parse market state
93        let market_state = match get_field("MARKET_STATE").as_deref() {
94            Some("closed") => Some(MarketState::Closed),
95            Some("offline") => Some(MarketState::Offline),
96            Some("tradeable") => Some(MarketState::Tradeable),
97            Some("edit") => Some(MarketState::Edit),
98            Some("auction") => Some(MarketState::Auction),
99            Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
100            Some("suspended") => Some(MarketState::Suspended),
101            Some(unknown) => return Err(format!("Unknown market state: {}", unknown)),
102            None => None,
103        };
104
105        // Parse boolean field
106        let market_delay = match get_field("MARKET_DELAY").as_deref() {
107            Some("0") => Some(false),
108            Some("1") => Some(true),
109            Some(val) => return Err(format!("Invalid MARKET_DELAY value: {}", val)),
110            None => None,
111        };
112
113        // Helper function to parse float values
114        let parse_float = |key: &str| -> Result<Option<f64>, String> {
115            match get_field(key) {
116                Some(val) if !val.is_empty() => val
117                    .parse::<f64>()
118                    .map(Some)
119                    .map_err(|_| format!("Failed to parse {} as float: {}", key, val)),
120                _ => Ok(None),
121            }
122        };
123
124        Ok(MarketFields {
125            mid_open: parse_float("MID_OPEN")?,
126            high: parse_float("HIGH")?,
127            offer: parse_float("OFFER")?,
128            change: parse_float("CHANGE")?,
129            market_delay,
130            low: parse_float("LOW")?,
131            bid: parse_float("BID")?,
132            change_pct: parse_float("CHANGE_PCT")?,
133            market_state,
134            update_time: get_field("UPDATE_TIME"),
135        })
136    }
137}
138
139impl fmt::Display for MarketData {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
142        write!(f, "{}", json)
143    }
144}
145
146impl From<&ItemUpdate> for MarketData {
147    fn from(item_update: &ItemUpdate) -> Self {
148        Self::from_item_update(item_update).unwrap_or_else(|_| MarketData {
149            item_name: String::new(),
150            item_pos: 0,
151            fields: MarketFields::default(),
152            changed_fields: MarketFields::default(),
153            is_snapshot: false,
154        })
155    }
156}
157
158/// Fields containing market price and status information
159#[derive(Debug, Clone, Serialize, Deserialize, Default)]
160pub struct MarketFields {
161    #[serde(rename = "MID_OPEN")]
162    #[serde(with = "string_as_float_opt")]
163    #[serde(default)]
164    mid_open: Option<f64>,
165
166    #[serde(rename = "HIGH")]
167    #[serde(with = "string_as_float_opt")]
168    #[serde(default)]
169    high: Option<f64>,
170
171    #[serde(rename = "OFFER")]
172    #[serde(with = "string_as_float_opt")]
173    #[serde(default)]
174    offer: Option<f64>,
175
176    #[serde(rename = "CHANGE")]
177    #[serde(with = "string_as_float_opt")]
178    #[serde(default)]
179    change: Option<f64>,
180
181    #[serde(rename = "MARKET_DELAY")]
182    #[serde(with = "string_as_bool_opt")]
183    #[serde(default)]
184    market_delay: Option<bool>,
185
186    #[serde(rename = "LOW")]
187    #[serde(with = "string_as_float_opt")]
188    #[serde(default)]
189    low: Option<f64>,
190
191    #[serde(rename = "BID")]
192    #[serde(with = "string_as_float_opt")]
193    #[serde(default)]
194    bid: Option<f64>,
195
196    #[serde(rename = "CHANGE_PCT")]
197    #[serde(with = "string_as_float_opt")]
198    #[serde(default)]
199    change_pct: Option<f64>,
200
201    #[serde(rename = "MARKET_STATE")]
202    #[serde(default)]
203    market_state: Option<MarketState>,
204
205    #[serde(rename = "UPDATE_TIME")]
206    #[serde(default)]
207    update_time: Option<String>,
208}