ig_client/presentation/
trade.rs

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