ig_client/presentation/
trade.rs1use 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 serde::{Deserialize, Serialize};
5use serde_json;
6use std::collections::HashMap;
7use std::fmt;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct TradeData {
13 pub item_name: String,
15 pub item_pos: i32,
17 pub fields: TradeFields,
19 pub changed_fields: TradeFields,
21 pub is_snapshot: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct TradeFields {
28 #[serde(rename = "CONFIRMS")]
30 #[serde(with = "option_string_empty_as_none")]
31 #[serde(default)]
32 pub confirms: Option<String>,
33 #[serde(rename = "OPU")]
35 #[serde(default)]
36 pub opu: Option<OpenPositionUpdate>,
37 #[serde(rename = "WOU")]
39 #[serde(default)]
40 pub wou: Option<WorkingOrderUpdate>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
45pub struct OpenPositionUpdate {
46 #[serde(rename = "dealReference")]
48 #[serde(with = "option_string_empty_as_none")]
49 #[serde(default)]
50 pub deal_reference: Option<String>,
51 #[serde(rename = "dealId")]
53 #[serde(with = "option_string_empty_as_none")]
54 #[serde(default)]
55 pub deal_id: Option<String>,
56 #[serde(default)]
58 pub direction: Option<Direction>,
59 #[serde(default)]
61 pub epic: Option<String>,
62 #[serde(default)]
64 pub status: Option<Status>,
65 #[serde(rename = "dealStatus")]
67 #[serde(default)]
68 pub deal_status: Option<Status>,
69 #[serde(with = "string_as_float_opt")]
71 #[serde(default)]
72 pub level: Option<f64>,
73 #[serde(with = "string_as_float_opt")]
75 #[serde(default)]
76 pub size: Option<f64>,
77 #[serde(with = "option_string_empty_as_none")]
79 #[serde(default)]
80 pub currency: Option<String>,
81 #[serde(with = "option_string_empty_as_none")]
83 #[serde(default)]
84 pub timestamp: Option<String>,
85 #[serde(with = "option_string_empty_as_none")]
87 #[serde(default)]
88 pub channel: Option<String>,
89 #[serde(with = "option_string_empty_as_none")]
91 #[serde(default)]
92 pub expiry: Option<String>,
93 #[serde(rename = "dealIdOrigin")]
95 #[serde(with = "option_string_empty_as_none")]
96 #[serde(default)]
97 pub deal_id_origin: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, Default)]
102pub struct WorkingOrderUpdate {
103 #[serde(rename = "dealReference")]
105 #[serde(with = "option_string_empty_as_none")]
106 #[serde(default)]
107 pub deal_reference: Option<String>,
108 #[serde(rename = "dealId")]
110 #[serde(with = "option_string_empty_as_none")]
111 #[serde(default)]
112 pub deal_id: Option<String>,
113 #[serde(default)]
115 pub direction: Option<Direction>,
116 #[serde(with = "option_string_empty_as_none")]
118 #[serde(default)]
119 pub epic: Option<String>,
120 #[serde(default)]
122 pub status: Option<Status>,
123 #[serde(rename = "dealStatus")]
125 #[serde(default)]
126 pub deal_status: Option<Status>,
127 #[serde(with = "string_as_float_opt")]
129 #[serde(default)]
130 pub level: Option<f64>,
131 #[serde(with = "string_as_float_opt")]
133 #[serde(default)]
134 pub size: Option<f64>,
135 #[serde(with = "option_string_empty_as_none")]
137 #[serde(default)]
138 pub currency: Option<String>,
139 #[serde(with = "option_string_empty_as_none")]
141 #[serde(default)]
142 pub timestamp: Option<String>,
143 #[serde(with = "option_string_empty_as_none")]
145 #[serde(default)]
146 pub channel: Option<String>,
147 #[serde(with = "option_string_empty_as_none")]
149 #[serde(default)]
150 pub expiry: Option<String>,
151 #[serde(rename = "stopDistance")]
153 #[serde(with = "string_as_float_opt")]
154 #[serde(default)]
155 pub stop_distance: Option<f64>,
156 #[serde(rename = "limitDistance")]
158 #[serde(with = "string_as_float_opt")]
159 #[serde(default)]
160 pub limit_distance: Option<f64>,
161 #[serde(rename = "guaranteedStop")]
163 #[serde(default)]
164 pub guaranteed_stop: Option<bool>,
165 #[serde(rename = "orderType")]
167 #[serde(default)]
168 pub order_type: Option<OrderType>,
169 #[serde(rename = "timeInForce")]
171 #[serde(default)]
172 pub time_in_force: Option<TimeInForce>,
173 #[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 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
191 let item_name = item_update.item_name.clone().unwrap_or_default();
193
194 let item_pos = item_update.item_pos as i32;
196
197 let is_snapshot = item_update.is_snapshot;
199
200 let fields = Self::create_trade_fields(&item_update.fields)?;
202
203 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 fn create_trade_fields(
221 fields_map: &HashMap<String, Option<String>>,
222 ) -> Result<TradeFields, String> {
223 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 let confirms = get_field("CONFIRMS");
234
235 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 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}