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#[derive(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
28impl fmt::Debug for ChartScale {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let s = match self {
31 ChartScale::Second => "SECOND",
32 ChartScale::OneMinute => "1MINUTE",
33 ChartScale::FiveMinute => "5MINUTE",
34 ChartScale::Hour => "HOUR",
35 ChartScale::Tick => "TICK",
36 };
37 write!(f, "{}", s)
38 }
39}
40
41impl fmt::Display for ChartScale {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 write!(f, "{:?}", self)
44 }
45}
46
47#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
48pub struct ChartData {
51 pub item_name: String,
53 pub item_pos: i32,
55 #[serde(default)]
57 pub scale: ChartScale, pub fields: ChartFields,
60 pub changed_fields: ChartFields,
62 pub is_snapshot: bool,
64}
65
66#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
68pub struct ChartFields {
69 #[serde(rename = "LTV")]
71 #[serde(with = "string_as_float_opt")]
72 #[serde(default)]
73 pub last_traded_volume: Option<f64>,
75
76 #[serde(rename = "TTV")]
77 #[serde(with = "string_as_float_opt")]
78 #[serde(default)]
79 pub incremental_trading_volume: Option<f64>,
81
82 #[serde(rename = "UTM")]
83 #[serde(with = "string_as_float_opt")]
84 #[serde(default)]
85 pub update_time: Option<f64>,
87
88 #[serde(rename = "DAY_OPEN_MID")]
89 #[serde(with = "string_as_float_opt")]
90 #[serde(default)]
91 pub day_open_mid: Option<f64>,
93
94 #[serde(rename = "DAY_NET_CHG_MID")]
95 #[serde(with = "string_as_float_opt")]
96 #[serde(default)]
97 pub day_net_change_mid: Option<f64>,
99
100 #[serde(rename = "DAY_PERC_CHG_MID")]
101 #[serde(with = "string_as_float_opt")]
102 #[serde(default)]
103 pub day_percentage_change_mid: Option<f64>,
105
106 #[serde(rename = "DAY_HIGH")]
107 #[serde(with = "string_as_float_opt")]
108 #[serde(default)]
109 pub day_high: Option<f64>,
111
112 #[serde(rename = "DAY_LOW")]
113 #[serde(with = "string_as_float_opt")]
114 #[serde(default)]
115 pub day_low: Option<f64>,
117
118 #[serde(rename = "BID")]
120 #[serde(with = "string_as_float_opt")]
121 #[serde(default)]
122 pub bid: Option<f64>,
124
125 #[serde(rename = "OFR")]
126 #[serde(with = "string_as_float_opt")]
127 #[serde(default)]
128 pub offer: Option<f64>,
130
131 #[serde(rename = "LTP")]
132 #[serde(with = "string_as_float_opt")]
133 #[serde(default)]
134 pub last_traded_price: Option<f64>,
136
137 #[serde(rename = "OFR_OPEN")]
139 #[serde(with = "string_as_float_opt")]
140 #[serde(default)]
141 pub offer_open: Option<f64>,
143
144 #[serde(rename = "OFR_HIGH")]
145 #[serde(with = "string_as_float_opt")]
146 #[serde(default)]
147 pub offer_high: Option<f64>,
149
150 #[serde(rename = "OFR_LOW")]
151 #[serde(with = "string_as_float_opt")]
152 #[serde(default)]
153 pub offer_low: Option<f64>,
155
156 #[serde(rename = "OFR_CLOSE")]
157 #[serde(with = "string_as_float_opt")]
158 #[serde(default)]
159 pub offer_close: Option<f64>,
161
162 #[serde(rename = "BID_OPEN")]
163 #[serde(with = "string_as_float_opt")]
164 #[serde(default)]
165 pub bid_open: Option<f64>,
167
168 #[serde(rename = "BID_HIGH")]
169 #[serde(with = "string_as_float_opt")]
170 #[serde(default)]
171 pub bid_high: Option<f64>,
173
174 #[serde(rename = "BID_LOW")]
175 #[serde(with = "string_as_float_opt")]
176 #[serde(default)]
177 pub bid_low: Option<f64>,
179
180 #[serde(rename = "BID_CLOSE")]
181 #[serde(with = "string_as_float_opt")]
182 #[serde(default)]
183 pub bid_close: Option<f64>,
185
186 #[serde(rename = "LTP_OPEN")]
187 #[serde(with = "string_as_float_opt")]
188 #[serde(default)]
189 pub ltp_open: Option<f64>,
191
192 #[serde(rename = "LTP_HIGH")]
193 #[serde(with = "string_as_float_opt")]
194 #[serde(default)]
195 pub ltp_high: Option<f64>,
197
198 #[serde(rename = "LTP_LOW")]
199 #[serde(with = "string_as_float_opt")]
200 #[serde(default)]
201 pub ltp_low: Option<f64>,
203
204 #[serde(rename = "LTP_CLOSE")]
205 #[serde(with = "string_as_float_opt")]
206 #[serde(default)]
207 pub ltp_close: Option<f64>,
209
210 #[serde(rename = "CONS_END")]
211 #[serde(with = "string_as_float_opt")]
212 #[serde(default)]
213 pub candle_end: Option<f64>,
215
216 #[serde(rename = "CONS_TICK_COUNT")]
217 #[serde(with = "string_as_float_opt")]
218 #[serde(default)]
219 pub candle_tick_count: Option<f64>,
221}
222
223impl ChartData {
224 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
234 let item_name = item_update.item_name.clone().unwrap_or_default();
236
237 let scale = if let Some(item_name) = &item_update.item_name {
239 if item_name.ends_with(":TICK") {
240 ChartScale::Tick
241 } else if item_name.ends_with(":SECOND") {
242 ChartScale::Second
243 } else if item_name.ends_with(":1MINUTE") {
244 ChartScale::OneMinute
245 } else if item_name.ends_with(":5MINUTE") {
246 ChartScale::FiveMinute
247 } else if item_name.ends_with(":HOUR") {
248 ChartScale::Hour
249 } else {
250 match item_update.fields.get("{scale}").and_then(|s| s.as_ref()) {
252 Some(s) if s == "SECOND" => ChartScale::Second,
253 Some(s) if s == "1MINUTE" => ChartScale::OneMinute,
254 Some(s) if s == "5MINUTE" => ChartScale::FiveMinute,
255 Some(s) if s == "HOUR" => ChartScale::Hour,
256 _ => ChartScale::Tick, }
258 }
259 } else {
260 ChartScale::default()
261 };
262
263 let item_pos = item_update.item_pos as i32;
265
266 let is_snapshot = item_update.is_snapshot;
268
269 let fields = Self::create_chart_fields(&item_update.fields)?;
271
272 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
274 for (key, value) in &item_update.changed_fields {
275 changed_fields_map.insert(key.clone(), Some(value.clone()));
276 }
277 let changed_fields = Self::create_chart_fields(&changed_fields_map)?;
278
279 Ok(ChartData {
280 item_name,
281 item_pos,
282 scale,
283 fields,
284 changed_fields,
285 is_snapshot,
286 })
287 }
288
289 fn create_chart_fields(
291 fields_map: &HashMap<String, Option<String>>,
292 ) -> Result<ChartFields, String> {
293 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
295
296 let parse_float = |key: &str| -> Result<Option<f64>, String> {
298 match get_field(key) {
299 Some(val) if !val.is_empty() => val
300 .parse::<f64>()
301 .map(Some)
302 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
303 _ => Ok(None),
304 }
305 };
306
307 Ok(ChartFields {
308 last_traded_volume: parse_float("LTV")?,
310 incremental_trading_volume: parse_float("TTV")?,
311 update_time: parse_float("UTM")?,
312 day_open_mid: parse_float("DAY_OPEN_MID")?,
313 day_net_change_mid: parse_float("DAY_NET_CHG_MID")?,
314 day_percentage_change_mid: parse_float("DAY_PERC_CHG_MID")?,
315 day_high: parse_float("DAY_HIGH")?,
316 day_low: parse_float("DAY_LOW")?,
317
318 bid: parse_float("BID")?,
320 offer: parse_float("OFR")?,
321 last_traded_price: parse_float("LTP")?,
322
323 offer_open: parse_float("OFR_OPEN")?,
325 offer_high: parse_float("OFR_HIGH")?,
326 offer_low: parse_float("OFR_LOW")?,
327 offer_close: parse_float("OFR_CLOSE")?,
328 bid_open: parse_float("BID_OPEN")?,
329 bid_high: parse_float("BID_HIGH")?,
330 bid_low: parse_float("BID_LOW")?,
331 bid_close: parse_float("BID_CLOSE")?,
332 ltp_open: parse_float("LTP_OPEN")?,
333 ltp_high: parse_float("LTP_HIGH")?,
334 ltp_low: parse_float("LTP_LOW")?,
335 ltp_close: parse_float("LTP_CLOSE")?,
336 candle_end: parse_float("CONS_END")?,
337 candle_tick_count: parse_float("CONS_TICK_COUNT")?,
338 })
339 }
340
341 pub fn is_tick(&self) -> bool {
343 matches!(self.scale, ChartScale::Tick)
344 }
345
346 pub fn is_candle(&self) -> bool {
348 !self.is_tick()
349 }
350
351 pub fn get_scale(&self) -> &ChartScale {
353 &self.scale
354 }
355}
356
357impl From<&ItemUpdate> for ChartData {
358 fn from(item_update: &ItemUpdate) -> Self {
359 Self::from_item_update(item_update).unwrap_or_default()
360 }
361}