1use crate::presentation::serialization::string_as_float_opt;
2use lightstreamer_rs::subscription::ItemUpdate;
3use pretty_simple_display::{DebugPretty, DisplaySimple};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
9pub enum ChartScale {
10 #[serde(rename = "SECOND")]
12 Second,
13 #[serde(rename = "1MINUTE")]
15 OneMinute,
16 #[serde(rename = "5MINUTE")]
18 FiveMinute,
19 #[serde(rename = "HOUR")]
21 Hour,
22 #[serde(rename = "TICK")]
24 #[default]
25 Tick,
26}
27
28#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
29pub struct ChartData {
32 pub item_name: String,
34 pub item_pos: i32,
36 #[serde(default)]
38 pub scale: ChartScale, pub fields: ChartFields,
41 pub changed_fields: ChartFields,
43 pub is_snapshot: bool,
45}
46
47#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
49pub struct ChartFields {
50 #[serde(rename = "LTV")]
52 #[serde(with = "string_as_float_opt")]
53 #[serde(default)]
54 pub last_traded_volume: Option<f64>,
56
57 #[serde(rename = "TTV")]
58 #[serde(with = "string_as_float_opt")]
59 #[serde(default)]
60 pub incremental_trading_volume: Option<f64>,
62
63 #[serde(rename = "UTM")]
64 #[serde(with = "string_as_float_opt")]
65 #[serde(default)]
66 pub update_time: Option<f64>,
68
69 #[serde(rename = "DAY_OPEN_MID")]
70 #[serde(with = "string_as_float_opt")]
71 #[serde(default)]
72 pub day_open_mid: Option<f64>,
74
75 #[serde(rename = "DAY_NET_CHG_MID")]
76 #[serde(with = "string_as_float_opt")]
77 #[serde(default)]
78 pub day_net_change_mid: Option<f64>,
80
81 #[serde(rename = "DAY_PERC_CHG_MID")]
82 #[serde(with = "string_as_float_opt")]
83 #[serde(default)]
84 pub day_percentage_change_mid: Option<f64>,
86
87 #[serde(rename = "DAY_HIGH")]
88 #[serde(with = "string_as_float_opt")]
89 #[serde(default)]
90 pub day_high: Option<f64>,
92
93 #[serde(rename = "DAY_LOW")]
94 #[serde(with = "string_as_float_opt")]
95 #[serde(default)]
96 pub day_low: Option<f64>,
98
99 #[serde(rename = "BID")]
101 #[serde(with = "string_as_float_opt")]
102 #[serde(default)]
103 pub bid: Option<f64>,
105
106 #[serde(rename = "OFR")]
107 #[serde(with = "string_as_float_opt")]
108 #[serde(default)]
109 pub offer: Option<f64>,
111
112 #[serde(rename = "LTP")]
113 #[serde(with = "string_as_float_opt")]
114 #[serde(default)]
115 pub last_traded_price: Option<f64>,
117
118 #[serde(rename = "OFR_OPEN")]
120 #[serde(with = "string_as_float_opt")]
121 #[serde(default)]
122 pub offer_open: Option<f64>,
124
125 #[serde(rename = "OFR_HIGH")]
126 #[serde(with = "string_as_float_opt")]
127 #[serde(default)]
128 pub offer_high: Option<f64>,
130
131 #[serde(rename = "OFR_LOW")]
132 #[serde(with = "string_as_float_opt")]
133 #[serde(default)]
134 pub offer_low: Option<f64>,
136
137 #[serde(rename = "OFR_CLOSE")]
138 #[serde(with = "string_as_float_opt")]
139 #[serde(default)]
140 pub offer_close: Option<f64>,
142
143 #[serde(rename = "BID_OPEN")]
144 #[serde(with = "string_as_float_opt")]
145 #[serde(default)]
146 pub bid_open: Option<f64>,
148
149 #[serde(rename = "BID_HIGH")]
150 #[serde(with = "string_as_float_opt")]
151 #[serde(default)]
152 pub bid_high: Option<f64>,
154
155 #[serde(rename = "BID_LOW")]
156 #[serde(with = "string_as_float_opt")]
157 #[serde(default)]
158 pub bid_low: Option<f64>,
160
161 #[serde(rename = "BID_CLOSE")]
162 #[serde(with = "string_as_float_opt")]
163 #[serde(default)]
164 pub bid_close: Option<f64>,
166
167 #[serde(rename = "LTP_OPEN")]
168 #[serde(with = "string_as_float_opt")]
169 #[serde(default)]
170 pub ltp_open: Option<f64>,
172
173 #[serde(rename = "LTP_HIGH")]
174 #[serde(with = "string_as_float_opt")]
175 #[serde(default)]
176 pub ltp_high: Option<f64>,
178
179 #[serde(rename = "LTP_LOW")]
180 #[serde(with = "string_as_float_opt")]
181 #[serde(default)]
182 pub ltp_low: Option<f64>,
184
185 #[serde(rename = "LTP_CLOSE")]
186 #[serde(with = "string_as_float_opt")]
187 #[serde(default)]
188 pub ltp_close: Option<f64>,
190
191 #[serde(rename = "CONS_END")]
192 #[serde(with = "string_as_float_opt")]
193 #[serde(default)]
194 pub candle_end: Option<f64>,
196
197 #[serde(rename = "CONS_TICK_COUNT")]
198 #[serde(with = "string_as_float_opt")]
199 #[serde(default)]
200 pub candle_tick_count: Option<f64>,
202}
203
204impl ChartData {
205 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
215 let item_name = item_update.item_name.clone().unwrap_or_default();
217
218 let scale = if let Some(item_name) = &item_update.item_name {
220 if item_name.ends_with(":TICK") {
221 ChartScale::Tick
222 } else if item_name.ends_with(":SECOND") {
223 ChartScale::Second
224 } else if item_name.ends_with(":1MINUTE") {
225 ChartScale::OneMinute
226 } else if item_name.ends_with(":5MINUTE") {
227 ChartScale::FiveMinute
228 } else if item_name.ends_with(":HOUR") {
229 ChartScale::Hour
230 } else {
231 match item_update.fields.get("{scale}").and_then(|s| s.as_ref()) {
233 Some(s) if s == "SECOND" => ChartScale::Second,
234 Some(s) if s == "1MINUTE" => ChartScale::OneMinute,
235 Some(s) if s == "5MINUTE" => ChartScale::FiveMinute,
236 Some(s) if s == "HOUR" => ChartScale::Hour,
237 _ => ChartScale::Tick, }
239 }
240 } else {
241 ChartScale::default()
242 };
243
244 let item_pos = item_update.item_pos as i32;
246
247 let is_snapshot = item_update.is_snapshot;
249
250 let fields = Self::create_chart_fields(&item_update.fields)?;
252
253 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
255 for (key, value) in &item_update.changed_fields {
256 changed_fields_map.insert(key.clone(), Some(value.clone()));
257 }
258 let changed_fields = Self::create_chart_fields(&changed_fields_map)?;
259
260 Ok(ChartData {
261 item_name,
262 item_pos,
263 scale,
264 fields,
265 changed_fields,
266 is_snapshot,
267 })
268 }
269
270 fn create_chart_fields(
272 fields_map: &HashMap<String, Option<String>>,
273 ) -> Result<ChartFields, String> {
274 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
276
277 let parse_float = |key: &str| -> Result<Option<f64>, String> {
279 match get_field(key) {
280 Some(val) if !val.is_empty() => val
281 .parse::<f64>()
282 .map(Some)
283 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
284 _ => Ok(None),
285 }
286 };
287
288 Ok(ChartFields {
289 last_traded_volume: parse_float("LTV")?,
291 incremental_trading_volume: parse_float("TTV")?,
292 update_time: parse_float("UTM")?,
293 day_open_mid: parse_float("DAY_OPEN_MID")?,
294 day_net_change_mid: parse_float("DAY_NET_CHG_MID")?,
295 day_percentage_change_mid: parse_float("DAY_PERC_CHG_MID")?,
296 day_high: parse_float("DAY_HIGH")?,
297 day_low: parse_float("DAY_LOW")?,
298
299 bid: parse_float("BID")?,
301 offer: parse_float("OFR")?,
302 last_traded_price: parse_float("LTP")?,
303
304 offer_open: parse_float("OFR_OPEN")?,
306 offer_high: parse_float("OFR_HIGH")?,
307 offer_low: parse_float("OFR_LOW")?,
308 offer_close: parse_float("OFR_CLOSE")?,
309 bid_open: parse_float("BID_OPEN")?,
310 bid_high: parse_float("BID_HIGH")?,
311 bid_low: parse_float("BID_LOW")?,
312 bid_close: parse_float("BID_CLOSE")?,
313 ltp_open: parse_float("LTP_OPEN")?,
314 ltp_high: parse_float("LTP_HIGH")?,
315 ltp_low: parse_float("LTP_LOW")?,
316 ltp_close: parse_float("LTP_CLOSE")?,
317 candle_end: parse_float("CONS_END")?,
318 candle_tick_count: parse_float("CONS_TICK_COUNT")?,
319 })
320 }
321
322 pub fn is_tick(&self) -> bool {
324 matches!(self.scale, ChartScale::Tick)
325 }
326
327 pub fn is_candle(&self) -> bool {
329 !self.is_tick()
330 }
331
332 pub fn get_scale(&self) -> &ChartScale {
334 &self.scale
335 }
336}
337
338impl From<&ItemUpdate> for ChartData {
339 fn from(item_update: &ItemUpdate) -> Self {
340 Self::from_item_update(item_update).unwrap_or_default()
341 }
342}