use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct MarketHierarchyNode {
pub id: String,
pub name: String,
pub parent_id: Option<String>,
pub exchange: String,
pub level: i32,
pub path: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct MarketInstrument {
pub epic: String,
pub instrument_name: String,
pub instrument_type: String,
pub node_id: String,
pub exchange: String,
pub expiry: String,
pub high_limit_price: Option<f64>,
pub low_limit_price: Option<f64>,
pub market_status: String,
pub net_change: Option<f64>,
pub percentage_change: Option<f64>,
pub update_time: Option<String>,
pub update_time_utc: Option<DateTime<Utc>>,
pub bid: Option<f64>,
pub offer: Option<f64>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl MarketInstrument {
pub fn is_call(&self) -> bool {
self.instrument_name.contains("CALL")
}
pub fn is_put(&self) -> bool {
self.instrument_name.contains("PUT")
}
}
impl MarketHierarchyNode {
pub fn new(
id: String,
name: String,
parent_id: Option<String>,
exchange: String,
level: i32,
path: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
parent_id,
exchange,
level,
path,
created_at: now,
updated_at: now,
}
}
pub fn build_path(parent_path: Option<&str>, node_name: &str) -> String {
match parent_path {
Some(parent) if !parent.is_empty() => format!("{parent}/{node_name}"),
_ => format!("/{node_name}"),
}
}
}
impl MarketInstrument {
pub fn new(
epic: String,
instrument_name: String,
instrument_type: String,
node_id: String,
exchange: String,
) -> Self {
let now = Utc::now();
Self {
epic,
instrument_name,
instrument_type,
node_id,
exchange,
expiry: String::new(),
high_limit_price: None,
low_limit_price: None,
market_status: String::new(),
net_change: None,
percentage_change: None,
update_time: None,
update_time_utc: None,
bid: None,
offer: None,
created_at: now,
updated_at: now,
}
}
pub fn parse_update_time_utc(&mut self) {
if let Some(ref time_str) = self.update_time
&& let Ok(parsed_time) = DateTime::parse_from_rfc3339(time_str)
{
self.update_time_utc = Some(parsed_time.with_timezone(&Utc));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_path() {
assert_eq!(MarketHierarchyNode::build_path(None, "Root"), "/Root");
assert_eq!(
MarketHierarchyNode::build_path(Some("/Root"), "Child"),
"/Root/Child"
);
assert_eq!(
MarketHierarchyNode::build_path(Some("/Root/Child"), "Grandchild"),
"/Root/Child/Grandchild"
);
}
#[test]
fn test_market_hierarchy_node_creation() {
let node = MarketHierarchyNode::new(
"test_id".to_string(),
"Test Node".to_string(),
Some("parent_id".to_string()),
"IG".to_string(),
1,
"/Test Node".to_string(),
);
assert_eq!(node.id, "test_id");
assert_eq!(node.name, "Test Node");
assert_eq!(node.parent_id, Some("parent_id".to_string()));
assert_eq!(node.exchange, "IG");
assert_eq!(node.level, 1);
assert_eq!(node.path, "/Test Node");
}
#[test]
fn test_market_instrument_creation() {
let mut instrument = MarketInstrument::new(
"IX.D.DAX.DAILY.IP".to_string(),
"Germany 40".to_string(),
"INDICES".to_string(),
"node_123".to_string(),
"IG".to_string(),
);
assert_eq!(instrument.epic, "IX.D.DAX.DAILY.IP");
assert_eq!(instrument.instrument_name, "Germany 40");
assert_eq!(instrument.instrument_type, "INDICES");
assert_eq!(instrument.node_id, "node_123");
assert_eq!(instrument.exchange, "IG");
instrument.update_time = Some("2023-12-01T10:30:00Z".to_string());
instrument.parse_update_time_utc();
assert!(instrument.update_time_utc.is_some());
}
}