1use crate::presentation::serialization::string_as_float_opt;
2use lightstreamer_rs::subscription::ItemUpdate;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
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(Debug, Clone, Serialize, Deserialize, Default)]
29pub struct ChartData {
32 item_name: String,
33 item_pos: i32,
34 #[serde(default)]
35 scale: ChartScale, fields: ChartFields,
37 changed_fields: ChartFields,
38 is_snapshot: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct ChartFields {
44 #[serde(rename = "LTV")]
46 #[serde(with = "string_as_float_opt")]
47 #[serde(default)]
48 last_traded_volume: Option<f64>,
49
50 #[serde(rename = "TTV")]
51 #[serde(with = "string_as_float_opt")]
52 #[serde(default)]
53 incremental_trading_volume: Option<f64>,
54
55 #[serde(rename = "UTM")]
56 #[serde(with = "string_as_float_opt")]
57 #[serde(default)]
58 update_time: Option<f64>,
59
60 #[serde(rename = "DAY_OPEN_MID")]
61 #[serde(with = "string_as_float_opt")]
62 #[serde(default)]
63 day_open_mid: Option<f64>,
64
65 #[serde(rename = "DAY_NET_CHG_MID")]
66 #[serde(with = "string_as_float_opt")]
67 #[serde(default)]
68 day_net_change_mid: Option<f64>,
69
70 #[serde(rename = "DAY_PERC_CHG_MID")]
71 #[serde(with = "string_as_float_opt")]
72 #[serde(default)]
73 day_percentage_change_mid: Option<f64>,
74
75 #[serde(rename = "DAY_HIGH")]
76 #[serde(with = "string_as_float_opt")]
77 #[serde(default)]
78 day_high: Option<f64>,
79
80 #[serde(rename = "DAY_LOW")]
81 #[serde(with = "string_as_float_opt")]
82 #[serde(default)]
83 day_low: Option<f64>,
84
85 #[serde(rename = "BID")]
87 #[serde(with = "string_as_float_opt")]
88 #[serde(default)]
89 bid: Option<f64>,
90
91 #[serde(rename = "OFR")]
92 #[serde(with = "string_as_float_opt")]
93 #[serde(default)]
94 offer: Option<f64>,
95
96 #[serde(rename = "LTP")]
97 #[serde(with = "string_as_float_opt")]
98 #[serde(default)]
99 last_traded_price: Option<f64>,
100
101 #[serde(rename = "OFR_OPEN")]
103 #[serde(with = "string_as_float_opt")]
104 #[serde(default)]
105 offer_open: Option<f64>,
106
107 #[serde(rename = "OFR_HIGH")]
108 #[serde(with = "string_as_float_opt")]
109 #[serde(default)]
110 offer_high: Option<f64>,
111
112 #[serde(rename = "OFR_LOW")]
113 #[serde(with = "string_as_float_opt")]
114 #[serde(default)]
115 offer_low: Option<f64>,
116
117 #[serde(rename = "OFR_CLOSE")]
118 #[serde(with = "string_as_float_opt")]
119 #[serde(default)]
120 offer_close: Option<f64>,
121
122 #[serde(rename = "BID_OPEN")]
123 #[serde(with = "string_as_float_opt")]
124 #[serde(default)]
125 bid_open: Option<f64>,
126
127 #[serde(rename = "BID_HIGH")]
128 #[serde(with = "string_as_float_opt")]
129 #[serde(default)]
130 bid_high: Option<f64>,
131
132 #[serde(rename = "BID_LOW")]
133 #[serde(with = "string_as_float_opt")]
134 #[serde(default)]
135 bid_low: Option<f64>,
136
137 #[serde(rename = "BID_CLOSE")]
138 #[serde(with = "string_as_float_opt")]
139 #[serde(default)]
140 bid_close: Option<f64>,
141
142 #[serde(rename = "LTP_OPEN")]
143 #[serde(with = "string_as_float_opt")]
144 #[serde(default)]
145 ltp_open: Option<f64>,
146
147 #[serde(rename = "LTP_HIGH")]
148 #[serde(with = "string_as_float_opt")]
149 #[serde(default)]
150 ltp_high: Option<f64>,
151
152 #[serde(rename = "LTP_LOW")]
153 #[serde(with = "string_as_float_opt")]
154 #[serde(default)]
155 ltp_low: Option<f64>,
156
157 #[serde(rename = "LTP_CLOSE")]
158 #[serde(with = "string_as_float_opt")]
159 #[serde(default)]
160 ltp_close: Option<f64>,
161
162 #[serde(rename = "CONS_END")]
163 #[serde(with = "string_as_float_opt")]
164 #[serde(default)]
165 candle_end: Option<f64>,
166
167 #[serde(rename = "CONS_TICK_COUNT")]
168 #[serde(with = "string_as_float_opt")]
169 #[serde(default)]
170 candle_tick_count: Option<f64>,
171}
172
173impl ChartData {
174 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
184 let item_name = item_update.item_name.clone().unwrap_or_default();
186
187 let scale = if let Some(item_name) = &item_update.item_name {
189 if item_name.ends_with(":TICK") {
190 ChartScale::Tick
191 } else if item_name.ends_with(":SECOND") {
192 ChartScale::Second
193 } else if item_name.ends_with(":1MINUTE") {
194 ChartScale::OneMinute
195 } else if item_name.ends_with(":5MINUTE") {
196 ChartScale::FiveMinute
197 } else if item_name.ends_with(":HOUR") {
198 ChartScale::Hour
199 } else {
200 match item_update.fields.get("{scale}").and_then(|s| s.as_ref()) {
202 Some(s) if s == "SECOND" => ChartScale::Second,
203 Some(s) if s == "1MINUTE" => ChartScale::OneMinute,
204 Some(s) if s == "5MINUTE" => ChartScale::FiveMinute,
205 Some(s) if s == "HOUR" => ChartScale::Hour,
206 _ => ChartScale::Tick, }
208 }
209 } else {
210 ChartScale::default()
211 };
212
213 let item_pos = item_update.item_pos as i32;
215
216 let is_snapshot = item_update.is_snapshot;
218
219 let fields = Self::create_chart_fields(&item_update.fields)?;
221
222 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
224 for (key, value) in &item_update.changed_fields {
225 changed_fields_map.insert(key.clone(), Some(value.clone()));
226 }
227 let changed_fields = Self::create_chart_fields(&changed_fields_map)?;
228
229 Ok(ChartData {
230 item_name,
231 item_pos,
232 scale,
233 fields,
234 changed_fields,
235 is_snapshot,
236 })
237 }
238
239 fn create_chart_fields(
241 fields_map: &HashMap<String, Option<String>>,
242 ) -> Result<ChartFields, String> {
243 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
245
246 let parse_float = |key: &str| -> Result<Option<f64>, String> {
248 match get_field(key) {
249 Some(val) if !val.is_empty() => val
250 .parse::<f64>()
251 .map(Some)
252 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
253 _ => Ok(None),
254 }
255 };
256
257 Ok(ChartFields {
258 last_traded_volume: parse_float("LTV")?,
260 incremental_trading_volume: parse_float("TTV")?,
261 update_time: parse_float("UTM")?,
262 day_open_mid: parse_float("DAY_OPEN_MID")?,
263 day_net_change_mid: parse_float("DAY_NET_CHG_MID")?,
264 day_percentage_change_mid: parse_float("DAY_PERC_CHG_MID")?,
265 day_high: parse_float("DAY_HIGH")?,
266 day_low: parse_float("DAY_LOW")?,
267
268 bid: parse_float("BID")?,
270 offer: parse_float("OFR")?,
271 last_traded_price: parse_float("LTP")?,
272
273 offer_open: parse_float("OFR_OPEN")?,
275 offer_high: parse_float("OFR_HIGH")?,
276 offer_low: parse_float("OFR_LOW")?,
277 offer_close: parse_float("OFR_CLOSE")?,
278 bid_open: parse_float("BID_OPEN")?,
279 bid_high: parse_float("BID_HIGH")?,
280 bid_low: parse_float("BID_LOW")?,
281 bid_close: parse_float("BID_CLOSE")?,
282 ltp_open: parse_float("LTP_OPEN")?,
283 ltp_high: parse_float("LTP_HIGH")?,
284 ltp_low: parse_float("LTP_LOW")?,
285 ltp_close: parse_float("LTP_CLOSE")?,
286 candle_end: parse_float("CONS_END")?,
287 candle_tick_count: parse_float("CONS_TICK_COUNT")?,
288 })
289 }
290
291 pub fn is_tick(&self) -> bool {
293 matches!(self.scale, ChartScale::Tick)
294 }
295
296 pub fn is_candle(&self) -> bool {
298 !self.is_tick()
299 }
300
301 pub fn get_scale(&self) -> &ChartScale {
303 &self.scale
304 }
305}
306
307impl fmt::Display for ChartData {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
310 write!(f, "{json}")
311 }
312}
313
314impl From<&ItemUpdate> for ChartData {
315 fn from(item_update: &ItemUpdate) -> Self {
316 Self::from_item_update(item_update).unwrap_or_default()
317 }
318}