ig_client/presentation/
trade.rs

1use crate::presentation::order::{Direction, OrderType, Status, TimeInForce};
2use crate::presentation::serialization::{option_string_empty_as_none, string_as_float_opt};
3use lightstreamer_rs::subscription::ItemUpdate;
4use pretty_simple_display::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use serde_json;
7use std::collections::HashMap;
8
9/// Main structure for trade data received from the IG Markets API
10/// Contains information about trades, positions and working orders
11#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
12pub struct TradeData {
13    /// Name of the subscribed item
14    pub item_name: String,
15    /// Position of the item in the subscription
16    pub item_pos: i32,
17    /// Trade fields data
18    pub fields: TradeFields,
19    /// Changed fields data
20    pub changed_fields: TradeFields,
21    /// Whether this is a snapshot
22    pub is_snapshot: bool,
23}
24
25/// Main fields for a trade update, containing core trade data.
26#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
27#[serde(rename_all = "UPPERCASE")]
28pub struct TradeFields {
29    /// Optional confirmation details for the trade.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub confirms: Option<String>,
32    /// Optional open position update details.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub opu: Option<OpenPositionUpdate>,
35    /// Optional working order update details.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub wou: Option<WorkingOrderUpdate>,
38}
39
40/// Structure representing details of an open position update.
41#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
42pub struct OpenPositionUpdate {
43    /// Unique deal reference for the open position.
44    #[serde(rename = "dealReference")]
45    #[serde(with = "option_string_empty_as_none")]
46    #[serde(default)]
47    pub deal_reference: Option<String>,
48    /// Unique deal identifier for the position.
49    #[serde(rename = "dealId")]
50    #[serde(with = "option_string_empty_as_none")]
51    #[serde(default)]
52    pub deal_id: Option<String>,
53    /// Direction of the trade position (buy or sell).
54    #[serde(default)]
55    pub direction: Option<Direction>,
56    /// Epic identifier for the instrument.
57    #[serde(default)]
58    pub epic: Option<String>,
59    /// Status of the position.
60    #[serde(default)]
61    pub status: Option<Status>,
62    /// Deal status of the position.
63    #[serde(rename = "dealStatus")]
64    #[serde(default)]
65    pub deal_status: Option<Status>,
66    /// Price level of the position.
67    #[serde(with = "string_as_float_opt")]
68    #[serde(default)]
69    pub level: Option<f64>,
70    /// Position size.
71    #[serde(with = "string_as_float_opt")]
72    #[serde(default)]
73    pub size: Option<f64>,
74    /// Currency of the position.
75    #[serde(with = "option_string_empty_as_none")]
76    #[serde(default)]
77    pub currency: Option<String>,
78    /// Timestamp of the position update.
79    #[serde(with = "option_string_empty_as_none")]
80    #[serde(default)]
81    pub timestamp: Option<String>,
82    /// Channel through which the update was received.
83    #[serde(with = "option_string_empty_as_none")]
84    #[serde(default)]
85    pub channel: Option<String>,
86    /// Expiry date of the position, if applicable.
87    #[serde(with = "option_string_empty_as_none")]
88    #[serde(default)]
89    pub expiry: Option<String>,
90    /// Original deal identifier for the position.
91    #[serde(rename = "dealIdOrigin")]
92    #[serde(with = "option_string_empty_as_none")]
93    #[serde(default)]
94    pub deal_id_origin: Option<String>,
95}
96
97/// Structure representing details of a working order update.
98#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
99pub struct WorkingOrderUpdate {
100    /// Unique deal reference for the working order.
101    #[serde(rename = "dealReference")]
102    #[serde(with = "option_string_empty_as_none")]
103    #[serde(default)]
104    pub deal_reference: Option<String>,
105    /// Unique deal identifier for the working order.
106    #[serde(rename = "dealId")]
107    #[serde(with = "option_string_empty_as_none")]
108    #[serde(default)]
109    pub deal_id: Option<String>,
110    /// Direction of the working order (buy or sell).
111    #[serde(default)]
112    pub direction: Option<Direction>,
113    /// Epic identifier for the working order instrument.
114    #[serde(with = "option_string_empty_as_none")]
115    #[serde(default)]
116    pub epic: Option<String>,
117    /// Status of the working order.
118    #[serde(default)]
119    pub status: Option<Status>,
120    /// Deal status of the working order.
121    #[serde(rename = "dealStatus")]
122    #[serde(default)]
123    pub deal_status: Option<Status>,
124    /// Price level at which the working order is set.
125    #[serde(with = "string_as_float_opt")]
126    #[serde(default)]
127    pub level: Option<f64>,
128    /// Working order size.
129    #[serde(with = "string_as_float_opt")]
130    #[serde(default)]
131    pub size: Option<f64>,
132    /// Currency of the working order.
133    #[serde(with = "option_string_empty_as_none")]
134    #[serde(default)]
135    pub currency: Option<String>,
136    /// Timestamp of the working order update.
137    #[serde(with = "option_string_empty_as_none")]
138    #[serde(default)]
139    pub timestamp: Option<String>,
140    /// Channel through which the working order update was received.
141    #[serde(with = "option_string_empty_as_none")]
142    #[serde(default)]
143    pub channel: Option<String>,
144    /// Expiry date of the working order.
145    #[serde(with = "option_string_empty_as_none")]
146    #[serde(default)]
147    pub expiry: Option<String>,
148    /// Stop distance for guaranteed stop orders.
149    #[serde(rename = "stopDistance")]
150    #[serde(with = "string_as_float_opt")]
151    #[serde(default)]
152    pub stop_distance: Option<f64>,
153    /// Limit distance for guaranteed stop orders.
154    #[serde(rename = "limitDistance")]
155    #[serde(with = "string_as_float_opt")]
156    #[serde(default)]
157    pub limit_distance: Option<f64>,
158    /// Whether the stop is guaranteed.
159    #[serde(rename = "guaranteedStop")]
160    #[serde(default)]
161    pub guaranteed_stop: Option<bool>,
162    /// Type of the order (e.g., market, limit).
163    #[serde(rename = "orderType")]
164    #[serde(default)]
165    pub order_type: Option<OrderType>,
166    /// Time in force for the order.
167    #[serde(rename = "timeInForce")]
168    #[serde(default)]
169    pub time_in_force: Option<TimeInForce>,
170    /// Good till date for the working order.
171    #[serde(rename = "goodTillDate")]
172    #[serde(with = "option_string_empty_as_none")]
173    #[serde(default)]
174    pub good_till_date: Option<String>,
175}
176
177impl TradeData {
178    /// Converts a Lightstreamer ItemUpdate to a TradeData object
179    ///
180    /// # Arguments
181    ///
182    /// * `item_update` - The ItemUpdate from Lightstreamer containing trade data
183    ///
184    /// # Returns
185    ///
186    /// A Result containing either the parsed TradeData or an error message
187    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
188        // Extract the item_name, defaulting to an empty string if None
189        let item_name = item_update.item_name.clone().unwrap_or_default();
190
191        // Convert item_pos from usize to i32
192        let item_pos = item_update.item_pos as i32;
193
194        // Extract is_snapshot
195        let is_snapshot = item_update.is_snapshot;
196
197        // Convert fields
198        let fields = Self::create_trade_fields(&item_update.fields)?;
199
200        // Convert changed_fields by first creating a HashMap<String, Option<String>>
201        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
202        for (key, value) in &item_update.changed_fields {
203            changed_fields_map.insert(key.clone(), Some(value.clone()));
204        }
205        let changed_fields = Self::create_trade_fields(&changed_fields_map)?;
206
207        Ok(TradeData {
208            item_name,
209            item_pos,
210            fields,
211            changed_fields,
212            is_snapshot,
213        })
214    }
215
216    // Helper method to create TradeFields from a HashMap
217    fn create_trade_fields(
218        fields_map: &HashMap<String, Option<String>>,
219    ) -> Result<TradeFields, String> {
220        // Helper function to safely get a field value
221        let get_field = |key: &str| -> Option<String> {
222            let field = fields_map.get(key).cloned().flatten();
223            match field {
224                Some(ref s) if s.is_empty() => None,
225                _ => field,
226            }
227        };
228
229        // Parse CONFIRMS
230        let confirms = get_field("CONFIRMS");
231
232        // Parse OPU
233        let opu_str = get_field("OPU");
234        let opu = if let Some(opu_json) = opu_str {
235            if !opu_json.is_empty() {
236                match serde_json::from_str::<OpenPositionUpdate>(&opu_json) {
237                    Ok(parsed_opu) => Some(parsed_opu),
238                    Err(e) => return Err(format!("Failed to parse OPU JSON: {e}")),
239                }
240            } else {
241                None
242            }
243        } else {
244            None
245        };
246        // Parse WOU
247        let wou_str = get_field("WOU");
248        let wou = if let Some(wou_json) = wou_str {
249            if !wou_json.is_empty() {
250                match serde_json::from_str::<WorkingOrderUpdate>(&wou_json) {
251                    Ok(parsed_wou) => Some(parsed_wou),
252                    Err(e) => return Err(format!("Failed to parse WOU JSON: {e}")),
253                }
254            } else {
255                None
256            }
257        } else {
258            None
259        };
260
261        Ok(TradeFields { confirms, opu, wou })
262    }
263}
264
265impl From<&ItemUpdate> for TradeData {
266    fn from(item_update: &ItemUpdate) -> Self {
267        Self::from_item_update(item_update).unwrap_or_default()
268    }
269}