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