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, fmt};
6
7#[repr(u8)]
9#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
10pub enum ChartScale {
11 #[serde(rename = "SECOND")]
13 Second,
14 #[serde(rename = "1MINUTE")]
16 OneMinute,
17 #[serde(rename = "5MINUTE")]
19 FiveMinute,
20 #[serde(rename = "HOUR")]
22 Hour,
23 #[serde(rename = "TICK")]
25 #[default]
26 Tick,
27}
28
29impl fmt::Debug for ChartScale {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 let s = match self {
32 ChartScale::Second => "SECOND",
33 ChartScale::OneMinute => "1MINUTE",
34 ChartScale::FiveMinute => "5MINUTE",
35 ChartScale::Hour => "HOUR",
36 ChartScale::Tick => "TICK",
37 };
38 write!(f, "{}", s)
39 }
40}
41
42impl fmt::Display for ChartScale {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(f, "{:?}", self)
45 }
46}
47
48#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
49pub struct ChartData {
52 pub item_name: String,
54 pub item_pos: i32,
56 #[serde(default)]
58 pub scale: ChartScale, pub fields: ChartFields,
61 pub changed_fields: ChartFields,
63 pub is_snapshot: bool,
65}
66
67#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
69pub struct ChartFields {
70 #[serde(rename = "LTV")]
72 #[serde(with = "string_as_float_opt")]
73 #[serde(default)]
74 pub last_traded_volume: Option<f64>,
76
77 #[serde(rename = "TTV")]
78 #[serde(with = "string_as_float_opt")]
79 #[serde(default)]
80 pub incremental_trading_volume: Option<f64>,
82
83 #[serde(rename = "UTM")]
84 #[serde(with = "string_as_float_opt")]
85 #[serde(default)]
86 pub update_time: Option<f64>,
88
89 #[serde(rename = "DAY_OPEN_MID")]
90 #[serde(with = "string_as_float_opt")]
91 #[serde(default)]
92 pub day_open_mid: Option<f64>,
94
95 #[serde(rename = "DAY_NET_CHG_MID")]
96 #[serde(with = "string_as_float_opt")]
97 #[serde(default)]
98 pub day_net_change_mid: Option<f64>,
100
101 #[serde(rename = "DAY_PERC_CHG_MID")]
102 #[serde(with = "string_as_float_opt")]
103 #[serde(default)]
104 pub day_percentage_change_mid: Option<f64>,
106
107 #[serde(rename = "DAY_HIGH")]
108 #[serde(with = "string_as_float_opt")]
109 #[serde(default)]
110 pub day_high: Option<f64>,
112
113 #[serde(rename = "DAY_LOW")]
114 #[serde(with = "string_as_float_opt")]
115 #[serde(default)]
116 pub day_low: Option<f64>,
118
119 #[serde(rename = "BID")]
121 #[serde(with = "string_as_float_opt")]
122 #[serde(default)]
123 pub bid: Option<f64>,
125
126 #[serde(rename = "OFR")]
127 #[serde(with = "string_as_float_opt")]
128 #[serde(default)]
129 pub offer: Option<f64>,
131
132 #[serde(rename = "LTP")]
133 #[serde(with = "string_as_float_opt")]
134 #[serde(default)]
135 pub last_traded_price: Option<f64>,
137
138 #[serde(rename = "OFR_OPEN")]
140 #[serde(with = "string_as_float_opt")]
141 #[serde(default)]
142 pub offer_open: Option<f64>,
144
145 #[serde(rename = "OFR_HIGH")]
146 #[serde(with = "string_as_float_opt")]
147 #[serde(default)]
148 pub offer_high: Option<f64>,
150
151 #[serde(rename = "OFR_LOW")]
152 #[serde(with = "string_as_float_opt")]
153 #[serde(default)]
154 pub offer_low: Option<f64>,
156
157 #[serde(rename = "OFR_CLOSE")]
158 #[serde(with = "string_as_float_opt")]
159 #[serde(default)]
160 pub offer_close: Option<f64>,
162
163 #[serde(rename = "BID_OPEN")]
164 #[serde(with = "string_as_float_opt")]
165 #[serde(default)]
166 pub bid_open: Option<f64>,
168
169 #[serde(rename = "BID_HIGH")]
170 #[serde(with = "string_as_float_opt")]
171 #[serde(default)]
172 pub bid_high: Option<f64>,
174
175 #[serde(rename = "BID_LOW")]
176 #[serde(with = "string_as_float_opt")]
177 #[serde(default)]
178 pub bid_low: Option<f64>,
180
181 #[serde(rename = "BID_CLOSE")]
182 #[serde(with = "string_as_float_opt")]
183 #[serde(default)]
184 pub bid_close: Option<f64>,
186
187 #[serde(rename = "LTP_OPEN")]
188 #[serde(with = "string_as_float_opt")]
189 #[serde(default)]
190 pub ltp_open: Option<f64>,
192
193 #[serde(rename = "LTP_HIGH")]
194 #[serde(with = "string_as_float_opt")]
195 #[serde(default)]
196 pub ltp_high: Option<f64>,
198
199 #[serde(rename = "LTP_LOW")]
200 #[serde(with = "string_as_float_opt")]
201 #[serde(default)]
202 pub ltp_low: Option<f64>,
204
205 #[serde(rename = "LTP_CLOSE")]
206 #[serde(with = "string_as_float_opt")]
207 #[serde(default)]
208 pub ltp_close: Option<f64>,
210
211 #[serde(rename = "CONS_END")]
212 #[serde(with = "string_as_float_opt")]
213 #[serde(default)]
214 pub candle_end: Option<f64>,
216
217 #[serde(rename = "CONS_TICK_COUNT")]
218 #[serde(with = "string_as_float_opt")]
219 #[serde(default)]
220 pub candle_tick_count: Option<f64>,
222}
223
224impl ChartData {
225 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
235 let item_name = item_update.item_name.clone().unwrap_or_default();
237
238 let scale = if let Some(item_name) = &item_update.item_name {
240 if item_name.ends_with(":TICK") {
241 ChartScale::Tick
242 } else if item_name.ends_with(":SECOND") {
243 ChartScale::Second
244 } else if item_name.ends_with(":1MINUTE") {
245 ChartScale::OneMinute
246 } else if item_name.ends_with(":5MINUTE") {
247 ChartScale::FiveMinute
248 } else if item_name.ends_with(":HOUR") {
249 ChartScale::Hour
250 } else {
251 match item_update.fields.get("{scale}").and_then(|s| s.as_ref()) {
253 Some(s) if s == "SECOND" => ChartScale::Second,
254 Some(s) if s == "1MINUTE" => ChartScale::OneMinute,
255 Some(s) if s == "5MINUTE" => ChartScale::FiveMinute,
256 Some(s) if s == "HOUR" => ChartScale::Hour,
257 _ => ChartScale::Tick, }
259 }
260 } else {
261 ChartScale::default()
262 };
263
264 let item_pos = item_update.item_pos as i32;
266
267 let is_snapshot = item_update.is_snapshot;
269
270 let fields = Self::create_chart_fields(&item_update.fields)?;
272
273 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
275 for (key, value) in &item_update.changed_fields {
276 changed_fields_map.insert(key.clone(), Some(value.clone()));
277 }
278 let changed_fields = Self::create_chart_fields(&changed_fields_map)?;
279
280 Ok(ChartData {
281 item_name,
282 item_pos,
283 scale,
284 fields,
285 changed_fields,
286 is_snapshot,
287 })
288 }
289
290 fn create_chart_fields(
292 fields_map: &HashMap<String, Option<String>>,
293 ) -> Result<ChartFields, String> {
294 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
296
297 let parse_float = |key: &str| -> Result<Option<f64>, String> {
299 match get_field(key) {
300 Some(val) if !val.is_empty() => val
301 .parse::<f64>()
302 .map(Some)
303 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
304 _ => Ok(None),
305 }
306 };
307
308 Ok(ChartFields {
309 last_traded_volume: parse_float("LTV")?,
311 incremental_trading_volume: parse_float("TTV")?,
312 update_time: parse_float("UTM")?,
313 day_open_mid: parse_float("DAY_OPEN_MID")?,
314 day_net_change_mid: parse_float("DAY_NET_CHG_MID")?,
315 day_percentage_change_mid: parse_float("DAY_PERC_CHG_MID")?,
316 day_high: parse_float("DAY_HIGH")?,
317 day_low: parse_float("DAY_LOW")?,
318
319 bid: parse_float("BID")?,
321 offer: parse_float("OFR")?,
322 last_traded_price: parse_float("LTP")?,
323
324 offer_open: parse_float("OFR_OPEN")?,
326 offer_high: parse_float("OFR_HIGH")?,
327 offer_low: parse_float("OFR_LOW")?,
328 offer_close: parse_float("OFR_CLOSE")?,
329 bid_open: parse_float("BID_OPEN")?,
330 bid_high: parse_float("BID_HIGH")?,
331 bid_low: parse_float("BID_LOW")?,
332 bid_close: parse_float("BID_CLOSE")?,
333 ltp_open: parse_float("LTP_OPEN")?,
334 ltp_high: parse_float("LTP_HIGH")?,
335 ltp_low: parse_float("LTP_LOW")?,
336 ltp_close: parse_float("LTP_CLOSE")?,
337 candle_end: parse_float("CONS_END")?,
338 candle_tick_count: parse_float("CONS_TICK_COUNT")?,
339 })
340 }
341
342 pub fn is_tick(&self) -> bool {
344 matches!(self.scale, ChartScale::Tick)
345 }
346
347 pub fn is_candle(&self) -> bool {
349 !self.is_tick()
350 }
351
352 pub fn get_scale(&self) -> &ChartScale {
354 &self.scale
355 }
356}
357
358impl From<&ItemUpdate> for ChartData {
359 fn from(item_update: &ItemUpdate) -> Self {
360 Self::from_item_update(item_update).unwrap_or_default()
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_chart_scale_default() {
370 let scale = ChartScale::default();
371 assert_eq!(scale, ChartScale::Tick);
372 }
373
374 #[test]
375 fn test_chart_scale_debug() {
376 assert_eq!(format!("{:?}", ChartScale::Second), "SECOND");
377 assert_eq!(format!("{:?}", ChartScale::OneMinute), "1MINUTE");
378 assert_eq!(format!("{:?}", ChartScale::FiveMinute), "5MINUTE");
379 assert_eq!(format!("{:?}", ChartScale::Hour), "HOUR");
380 assert_eq!(format!("{:?}", ChartScale::Tick), "TICK");
381 }
382
383 #[test]
384 fn test_chart_scale_display() {
385 assert_eq!(format!("{}", ChartScale::Second), "SECOND");
386 assert_eq!(format!("{}", ChartScale::Tick), "TICK");
387 }
388
389 #[test]
390 fn test_chart_scale_serialization() {
391 let scale = ChartScale::OneMinute;
392 let json = serde_json::to_string(&scale).expect("serialize failed");
393 assert_eq!(json, "\"1MINUTE\"");
394
395 let deserialized: ChartScale = serde_json::from_str(&json).expect("deserialize failed");
396 assert_eq!(deserialized, ChartScale::OneMinute);
397 }
398
399 #[test]
400 fn test_chart_data_default() {
401 let data = ChartData::default();
402 assert!(data.item_name.is_empty());
403 assert_eq!(data.item_pos, 0);
404 assert_eq!(data.scale, ChartScale::Tick);
405 assert!(!data.is_snapshot);
406 }
407
408 #[test]
409 fn test_chart_data_is_tick() {
410 let data = ChartData {
411 scale: ChartScale::Tick,
412 ..Default::default()
413 };
414 assert!(data.is_tick());
415 assert!(!data.is_candle());
416 }
417
418 #[test]
419 fn test_chart_data_is_candle() {
420 let data = ChartData {
421 scale: ChartScale::OneMinute,
422 ..Default::default()
423 };
424 assert!(!data.is_tick());
425 assert!(data.is_candle());
426
427 let data_hour = ChartData {
428 scale: ChartScale::Hour,
429 ..Default::default()
430 };
431 assert!(data_hour.is_candle());
432 }
433
434 #[test]
435 fn test_chart_data_get_scale() {
436 let data = ChartData {
437 scale: ChartScale::FiveMinute,
438 ..Default::default()
439 };
440 assert_eq!(*data.get_scale(), ChartScale::FiveMinute);
441 }
442
443 #[test]
444 fn test_chart_fields_default() {
445 let fields = ChartFields::default();
446 assert!(fields.bid.is_none());
447 assert!(fields.offer.is_none());
448 assert!(fields.last_traded_price.is_none());
449 assert!(fields.day_high.is_none());
450 assert!(fields.day_low.is_none());
451 }
452
453 #[test]
454 fn test_chart_fields_creation() {
455 let fields = ChartFields {
456 bid: Some(100.5),
457 offer: Some(101.0),
458 last_traded_price: Some(100.75),
459 day_high: Some(102.0),
460 day_low: Some(99.0),
461 ..Default::default()
462 };
463 assert_eq!(fields.bid, Some(100.5));
464 assert_eq!(fields.offer, Some(101.0));
465 assert_eq!(fields.last_traded_price, Some(100.75));
466 }
467
468 #[test]
469 fn test_chart_scale_equality() {
470 assert_eq!(ChartScale::Tick, ChartScale::Tick);
471 assert_ne!(ChartScale::Tick, ChartScale::Hour);
472 }
473
474 #[test]
475 fn test_chart_scale_hash() {
476 use std::collections::HashSet;
477 let mut set = HashSet::new();
478 set.insert(ChartScale::Tick);
479 set.insert(ChartScale::Tick);
480 assert_eq!(set.len(), 1);
481 }
482}