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