use crate::presentation::serialization::string_as_float_opt;
use lightstreamer_rs::subscription::ItemUpdate;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt};
#[repr(u8)]
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
pub enum ChartScale {
#[serde(rename = "SECOND")]
Second,
#[serde(rename = "1MINUTE")]
OneMinute,
#[serde(rename = "5MINUTE")]
FiveMinute,
#[serde(rename = "HOUR")]
Hour,
#[serde(rename = "TICK")]
#[default]
Tick,
}
impl fmt::Debug for ChartScale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ChartScale::Second => "SECOND",
ChartScale::OneMinute => "1MINUTE",
ChartScale::FiveMinute => "5MINUTE",
ChartScale::Hour => "HOUR",
ChartScale::Tick => "TICK",
};
write!(f, "{}", s)
}
}
impl fmt::Display for ChartScale {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
pub struct ChartData {
pub item_name: String,
pub item_pos: i32,
#[serde(default)]
pub scale: ChartScale, pub fields: ChartFields,
pub changed_fields: ChartFields,
pub is_snapshot: bool,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
pub struct ChartFields {
#[serde(rename = "LTV")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub last_traded_volume: Option<f64>,
#[serde(rename = "TTV")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub incremental_trading_volume: Option<f64>,
#[serde(rename = "UTM")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub update_time: Option<f64>,
#[serde(rename = "DAY_OPEN_MID")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub day_open_mid: Option<f64>,
#[serde(rename = "DAY_NET_CHG_MID")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub day_net_change_mid: Option<f64>,
#[serde(rename = "DAY_PERC_CHG_MID")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub day_percentage_change_mid: Option<f64>,
#[serde(rename = "DAY_HIGH")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub day_high: Option<f64>,
#[serde(rename = "DAY_LOW")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub day_low: Option<f64>,
#[serde(rename = "BID")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid: Option<f64>,
#[serde(rename = "OFR")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer: Option<f64>,
#[serde(rename = "LTP")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub last_traded_price: Option<f64>,
#[serde(rename = "OFR_OPEN")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer_open: Option<f64>,
#[serde(rename = "OFR_HIGH")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer_high: Option<f64>,
#[serde(rename = "OFR_LOW")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer_low: Option<f64>,
#[serde(rename = "OFR_CLOSE")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub offer_close: Option<f64>,
#[serde(rename = "BID_OPEN")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid_open: Option<f64>,
#[serde(rename = "BID_HIGH")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid_high: Option<f64>,
#[serde(rename = "BID_LOW")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid_low: Option<f64>,
#[serde(rename = "BID_CLOSE")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub bid_close: Option<f64>,
#[serde(rename = "LTP_OPEN")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub ltp_open: Option<f64>,
#[serde(rename = "LTP_HIGH")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub ltp_high: Option<f64>,
#[serde(rename = "LTP_LOW")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub ltp_low: Option<f64>,
#[serde(rename = "LTP_CLOSE")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub ltp_close: Option<f64>,
#[serde(rename = "CONS_END")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub candle_end: Option<f64>,
#[serde(rename = "CONS_TICK_COUNT")]
#[serde(with = "string_as_float_opt")]
#[serde(default)]
pub candle_tick_count: Option<f64>,
}
impl ChartData {
pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
let item_name = item_update.item_name.clone().unwrap_or_default();
let scale = if let Some(item_name) = &item_update.item_name {
if item_name.ends_with(":TICK") {
ChartScale::Tick
} else if item_name.ends_with(":SECOND") {
ChartScale::Second
} else if item_name.ends_with(":1MINUTE") {
ChartScale::OneMinute
} else if item_name.ends_with(":5MINUTE") {
ChartScale::FiveMinute
} else if item_name.ends_with(":HOUR") {
ChartScale::Hour
} else {
match item_update.fields.get("{scale}").and_then(|s| s.as_ref()) {
Some(s) if s == "SECOND" => ChartScale::Second,
Some(s) if s == "1MINUTE" => ChartScale::OneMinute,
Some(s) if s == "5MINUTE" => ChartScale::FiveMinute,
Some(s) if s == "HOUR" => ChartScale::Hour,
_ => ChartScale::Tick, }
}
} else {
ChartScale::default()
};
let item_pos = item_update.item_pos as i32;
let is_snapshot = item_update.is_snapshot;
let fields = Self::create_chart_fields(&item_update.fields)?;
let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
for (key, value) in &item_update.changed_fields {
changed_fields_map.insert(key.clone(), Some(value.clone()));
}
let changed_fields = Self::create_chart_fields(&changed_fields_map)?;
Ok(ChartData {
item_name,
item_pos,
scale,
fields,
changed_fields,
is_snapshot,
})
}
fn create_chart_fields(
fields_map: &HashMap<String, Option<String>>,
) -> Result<ChartFields, String> {
let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
let parse_float = |key: &str| -> Result<Option<f64>, String> {
match get_field(key) {
Some(val) if !val.is_empty() => val
.parse::<f64>()
.map(Some)
.map_err(|_| format!("Failed to parse {key} as float: {val}")),
_ => Ok(None),
}
};
Ok(ChartFields {
last_traded_volume: parse_float("LTV")?,
incremental_trading_volume: parse_float("TTV")?,
update_time: parse_float("UTM")?,
day_open_mid: parse_float("DAY_OPEN_MID")?,
day_net_change_mid: parse_float("DAY_NET_CHG_MID")?,
day_percentage_change_mid: parse_float("DAY_PERC_CHG_MID")?,
day_high: parse_float("DAY_HIGH")?,
day_low: parse_float("DAY_LOW")?,
bid: parse_float("BID")?,
offer: parse_float("OFR")?,
last_traded_price: parse_float("LTP")?,
offer_open: parse_float("OFR_OPEN")?,
offer_high: parse_float("OFR_HIGH")?,
offer_low: parse_float("OFR_LOW")?,
offer_close: parse_float("OFR_CLOSE")?,
bid_open: parse_float("BID_OPEN")?,
bid_high: parse_float("BID_HIGH")?,
bid_low: parse_float("BID_LOW")?,
bid_close: parse_float("BID_CLOSE")?,
ltp_open: parse_float("LTP_OPEN")?,
ltp_high: parse_float("LTP_HIGH")?,
ltp_low: parse_float("LTP_LOW")?,
ltp_close: parse_float("LTP_CLOSE")?,
candle_end: parse_float("CONS_END")?,
candle_tick_count: parse_float("CONS_TICK_COUNT")?,
})
}
pub fn is_tick(&self) -> bool {
matches!(self.scale, ChartScale::Tick)
}
pub fn is_candle(&self) -> bool {
!self.is_tick()
}
pub fn get_scale(&self) -> &ChartScale {
&self.scale
}
}
impl From<&ItemUpdate> for ChartData {
fn from(item_update: &ItemUpdate) -> Self {
Self::from_item_update(item_update).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chart_scale_default() {
let scale = ChartScale::default();
assert_eq!(scale, ChartScale::Tick);
}
#[test]
fn test_chart_scale_debug() {
assert_eq!(format!("{:?}", ChartScale::Second), "SECOND");
assert_eq!(format!("{:?}", ChartScale::OneMinute), "1MINUTE");
assert_eq!(format!("{:?}", ChartScale::FiveMinute), "5MINUTE");
assert_eq!(format!("{:?}", ChartScale::Hour), "HOUR");
assert_eq!(format!("{:?}", ChartScale::Tick), "TICK");
}
#[test]
fn test_chart_scale_display() {
assert_eq!(format!("{}", ChartScale::Second), "SECOND");
assert_eq!(format!("{}", ChartScale::Tick), "TICK");
}
#[test]
fn test_chart_scale_serialization() {
let scale = ChartScale::OneMinute;
let json = serde_json::to_string(&scale).expect("serialize failed");
assert_eq!(json, "\"1MINUTE\"");
let deserialized: ChartScale = serde_json::from_str(&json).expect("deserialize failed");
assert_eq!(deserialized, ChartScale::OneMinute);
}
#[test]
fn test_chart_data_default() {
let data = ChartData::default();
assert!(data.item_name.is_empty());
assert_eq!(data.item_pos, 0);
assert_eq!(data.scale, ChartScale::Tick);
assert!(!data.is_snapshot);
}
#[test]
fn test_chart_data_is_tick() {
let data = ChartData {
scale: ChartScale::Tick,
..Default::default()
};
assert!(data.is_tick());
assert!(!data.is_candle());
}
#[test]
fn test_chart_data_is_candle() {
let data = ChartData {
scale: ChartScale::OneMinute,
..Default::default()
};
assert!(!data.is_tick());
assert!(data.is_candle());
let data_hour = ChartData {
scale: ChartScale::Hour,
..Default::default()
};
assert!(data_hour.is_candle());
}
#[test]
fn test_chart_data_get_scale() {
let data = ChartData {
scale: ChartScale::FiveMinute,
..Default::default()
};
assert_eq!(*data.get_scale(), ChartScale::FiveMinute);
}
#[test]
fn test_chart_fields_default() {
let fields = ChartFields::default();
assert!(fields.bid.is_none());
assert!(fields.offer.is_none());
assert!(fields.last_traded_price.is_none());
assert!(fields.day_high.is_none());
assert!(fields.day_low.is_none());
}
#[test]
fn test_chart_fields_creation() {
let fields = ChartFields {
bid: Some(100.5),
offer: Some(101.0),
last_traded_price: Some(100.75),
day_high: Some(102.0),
day_low: Some(99.0),
..Default::default()
};
assert_eq!(fields.bid, Some(100.5));
assert_eq!(fields.offer, Some(101.0));
assert_eq!(fields.last_traded_price, Some(100.75));
}
#[test]
fn test_chart_scale_equality() {
assert_eq!(ChartScale::Tick, ChartScale::Tick);
assert_ne!(ChartScale::Tick, ChartScale::Hour);
}
#[test]
fn test_chart_scale_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ChartScale::Tick);
set.insert(ChartScale::Tick);
assert_eq!(set.len(), 1);
}
}