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 pretty_simple_display::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use serde_json;
7use std::collections::HashMap;
8
9#[derive(DebugPretty, DisplaySimple, 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(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
27#[serde(rename_all = "UPPERCASE")]
28pub struct TradeFields {
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub confirms: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub opu: Option<OpenPositionUpdate>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub wou: Option<WorkingOrderUpdate>,
38}
39
40#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
42pub struct OpenPositionUpdate {
43 #[serde(rename = "dealReference")]
45 #[serde(with = "option_string_empty_as_none")]
46 #[serde(default)]
47 pub deal_reference: Option<String>,
48 #[serde(rename = "dealId")]
50 #[serde(with = "option_string_empty_as_none")]
51 #[serde(default)]
52 pub deal_id: Option<String>,
53 #[serde(default)]
55 pub direction: Option<Direction>,
56 #[serde(default)]
58 pub epic: Option<String>,
59 #[serde(default)]
61 pub status: Option<Status>,
62 #[serde(rename = "dealStatus")]
64 #[serde(default)]
65 pub deal_status: Option<Status>,
66 #[serde(with = "string_as_float_opt")]
68 #[serde(default)]
69 pub level: Option<f64>,
70 #[serde(with = "string_as_float_opt")]
72 #[serde(default)]
73 pub size: Option<f64>,
74 #[serde(with = "option_string_empty_as_none")]
76 #[serde(default)]
77 pub currency: Option<String>,
78 #[serde(with = "option_string_empty_as_none")]
80 #[serde(default)]
81 pub timestamp: Option<String>,
82 #[serde(with = "option_string_empty_as_none")]
84 #[serde(default)]
85 pub channel: Option<String>,
86 #[serde(with = "option_string_empty_as_none")]
88 #[serde(default)]
89 pub expiry: Option<String>,
90 #[serde(rename = "dealIdOrigin")]
92 #[serde(with = "option_string_empty_as_none")]
93 #[serde(default)]
94 pub deal_id_origin: Option<String>,
95}
96
97#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
99pub struct WorkingOrderUpdate {
100 #[serde(rename = "dealReference")]
102 #[serde(with = "option_string_empty_as_none")]
103 #[serde(default)]
104 pub deal_reference: Option<String>,
105 #[serde(rename = "dealId")]
107 #[serde(with = "option_string_empty_as_none")]
108 #[serde(default)]
109 pub deal_id: Option<String>,
110 #[serde(default)]
112 pub direction: Option<Direction>,
113 #[serde(with = "option_string_empty_as_none")]
115 #[serde(default)]
116 pub epic: Option<String>,
117 #[serde(default)]
119 pub status: Option<Status>,
120 #[serde(rename = "dealStatus")]
122 #[serde(default)]
123 pub deal_status: Option<Status>,
124 #[serde(with = "string_as_float_opt")]
126 #[serde(default)]
127 pub level: Option<f64>,
128 #[serde(with = "string_as_float_opt")]
130 #[serde(default)]
131 pub size: Option<f64>,
132 #[serde(with = "option_string_empty_as_none")]
134 #[serde(default)]
135 pub currency: Option<String>,
136 #[serde(with = "option_string_empty_as_none")]
138 #[serde(default)]
139 pub timestamp: Option<String>,
140 #[serde(with = "option_string_empty_as_none")]
142 #[serde(default)]
143 pub channel: Option<String>,
144 #[serde(with = "option_string_empty_as_none")]
146 #[serde(default)]
147 pub expiry: Option<String>,
148 #[serde(rename = "stopDistance")]
150 #[serde(with = "string_as_float_opt")]
151 #[serde(default)]
152 pub stop_distance: Option<f64>,
153 #[serde(rename = "limitDistance")]
155 #[serde(with = "string_as_float_opt")]
156 #[serde(default)]
157 pub limit_distance: Option<f64>,
158 #[serde(rename = "guaranteedStop")]
160 #[serde(default)]
161 pub guaranteed_stop: Option<bool>,
162 #[serde(rename = "orderType")]
164 #[serde(default)]
165 pub order_type: Option<OrderType>,
166 #[serde(rename = "timeInForce")]
168 #[serde(default)]
169 pub time_in_force: Option<TimeInForce>,
170 #[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 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
188 let item_name = item_update.item_name.clone().unwrap_or_default();
190
191 let item_pos = item_update.item_pos as i32;
193
194 let is_snapshot = item_update.is_snapshot;
196
197 let fields = Self::create_trade_fields(&item_update.fields)?;
199
200 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 fn create_trade_fields(
218 fields_map: &HashMap<String, Option<String>>,
219 ) -> Result<TradeFields, String> {
220 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 let confirms = get_field("CONFIRMS");
231
232 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 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}