1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4
5#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
8pub struct MarketHierarchyNode {
9 pub id: String,
11 pub name: String,
13 pub parent_id: Option<String>,
15 pub exchange: String,
17 pub level: i32,
19 pub path: String,
21 pub created_at: DateTime<Utc>,
23 pub updated_at: DateTime<Utc>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
30pub struct MarketInstrument {
31 pub epic: String,
33 pub instrument_name: String,
35 pub instrument_type: String,
37 pub node_id: String,
39 pub exchange: String,
41 pub expiry: String,
43 pub high_limit_price: Option<f64>,
45 pub low_limit_price: Option<f64>,
47 pub market_status: String,
49 pub net_change: Option<f64>,
51 pub percentage_change: Option<f64>,
53 pub update_time: Option<String>,
55 pub update_time_utc: Option<DateTime<Utc>>,
57 pub bid: Option<f64>,
59 pub offer: Option<f64>,
61 pub created_at: DateTime<Utc>,
63 pub updated_at: DateTime<Utc>,
65}
66
67pub const CREATE_MARKET_HIERARCHY_TABLE: &str = r#"
69CREATE TABLE IF NOT EXISTS market_hierarchy_nodes (
70 id VARCHAR(255) PRIMARY KEY,
71 name VARCHAR(500) NOT NULL,
72 parent_id VARCHAR(255) REFERENCES market_hierarchy_nodes(id),
73 exchange VARCHAR(50) NOT NULL,
74 level INTEGER NOT NULL DEFAULT 0,
75 path TEXT NOT NULL,
76 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
78);
79
80-- Indexes for efficient querying
81CREATE INDEX IF NOT EXISTS idx_market_hierarchy_parent_id ON market_hierarchy_nodes(parent_id);
82CREATE INDEX IF NOT EXISTS idx_market_hierarchy_exchange ON market_hierarchy_nodes(exchange);
83CREATE INDEX IF NOT EXISTS idx_market_hierarchy_level ON market_hierarchy_nodes(level);
84CREATE INDEX IF NOT EXISTS idx_market_hierarchy_path ON market_hierarchy_nodes USING gin(to_tsvector('english', path));
85CREATE INDEX IF NOT EXISTS idx_market_hierarchy_name ON market_hierarchy_nodes USING gin(to_tsvector('english', name));
86"#;
87
88pub const CREATE_MARKET_INSTRUMENTS_TABLE: &str = r#"
92CREATE TABLE IF NOT EXISTS market_instruments (
93 epic VARCHAR(255) PRIMARY KEY,
94 instrument_name VARCHAR(500) NOT NULL,
95 instrument_type VARCHAR(100) NOT NULL,
96 node_id VARCHAR(255) NOT NULL REFERENCES market_hierarchy_nodes(id),
97 exchange VARCHAR(50) NOT NULL,
98 expiry VARCHAR(50) NOT NULL DEFAULT '',
99 high_limit_price DOUBLE PRECISION,
100 low_limit_price DOUBLE PRECISION,
101 market_status VARCHAR(50) NOT NULL,
102 net_change DOUBLE PRECISION,
103 percentage_change DOUBLE PRECISION,
104 update_time VARCHAR(50),
105 update_time_utc TIMESTAMPTZ,
106 bid DOUBLE PRECISION,
107 offer DOUBLE PRECISION,
108 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
109 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
110);
111
112-- Indexes for efficient querying
113CREATE INDEX IF NOT EXISTS idx_market_instruments_node_id ON market_instruments(node_id);
114CREATE INDEX IF NOT EXISTS idx_market_instruments_exchange ON market_instruments(exchange);
115CREATE INDEX IF NOT EXISTS idx_market_instruments_type ON market_instruments(instrument_type);
116CREATE INDEX IF NOT EXISTS idx_market_instruments_status ON market_instruments(market_status);
117CREATE INDEX IF NOT EXISTS idx_market_instruments_name ON market_instruments USING gin(to_tsvector('english', instrument_name));
118CREATE INDEX IF NOT EXISTS idx_market_instruments_epic ON market_instruments(epic);
119CREATE INDEX IF NOT EXISTS idx_market_instruments_expiry ON market_instruments(expiry);
120"#;
121
122pub const CREATE_UPDATE_TIMESTAMP_TRIGGER: &str = r#"
124-- Function to update the updated_at timestamp
125CREATE OR REPLACE FUNCTION update_updated_at_column()
126RETURNS TRIGGER AS $$
127BEGIN
128 NEW.updated_at = NOW();
129 RETURN NEW;
130END;
131$$ language 'plpgsql';
132
133-- Triggers for both tables
134DROP TRIGGER IF EXISTS update_market_hierarchy_nodes_updated_at ON market_hierarchy_nodes;
135CREATE TRIGGER update_market_hierarchy_nodes_updated_at
136 BEFORE UPDATE ON market_hierarchy_nodes
137 FOR EACH ROW
138 EXECUTE FUNCTION update_updated_at_column();
139
140DROP TRIGGER IF EXISTS update_market_instruments_updated_at ON market_instruments;
141CREATE TRIGGER update_market_instruments_updated_at
142 BEFORE UPDATE ON market_instruments
143 FOR EACH ROW
144 EXECUTE FUNCTION update_updated_at_column();
145"#;
146
147impl MarketHierarchyNode {
148 pub fn new(
150 id: String,
151 name: String,
152 parent_id: Option<String>,
153 exchange: String,
154 level: i32,
155 path: String,
156 ) -> Self {
157 let now = Utc::now();
158 Self {
159 id,
160 name,
161 parent_id,
162 exchange,
163 level,
164 path,
165 created_at: now,
166 updated_at: now,
167 }
168 }
169
170 pub fn build_path(parent_path: Option<&str>, node_name: &str) -> String {
172 match parent_path {
173 Some(parent) if !parent.is_empty() => format!("{parent}/{node_name}"),
174 _ => format!("/{node_name}"),
175 }
176 }
177}
178
179impl MarketInstrument {
180 pub fn new(
182 epic: String,
183 instrument_name: String,
184 instrument_type: String,
185 node_id: String,
186 exchange: String,
187 ) -> Self {
188 let now = Utc::now();
189 Self {
190 epic,
191 instrument_name,
192 instrument_type,
193 node_id,
194 exchange,
195 expiry: String::new(),
196 high_limit_price: None,
197 low_limit_price: None,
198 market_status: String::new(),
199 net_change: None,
200 percentage_change: None,
201 update_time: None,
202 update_time_utc: None,
203 bid: None,
204 offer: None,
205 created_at: now,
206 updated_at: now,
207 }
208 }
209
210 pub fn parse_update_time_utc(&mut self) {
212 if let Some(ref time_str) = self.update_time
213 && let Ok(parsed_time) = DateTime::parse_from_rfc3339(time_str)
214 {
215 self.update_time_utc = Some(parsed_time.with_timezone(&Utc));
216 }
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_build_path() {
226 assert_eq!(MarketHierarchyNode::build_path(None, "Root"), "/Root");
227 assert_eq!(
228 MarketHierarchyNode::build_path(Some("/Root"), "Child"),
229 "/Root/Child"
230 );
231 assert_eq!(
232 MarketHierarchyNode::build_path(Some("/Root/Child"), "Grandchild"),
233 "/Root/Child/Grandchild"
234 );
235 }
236
237 #[test]
238 fn test_market_hierarchy_node_creation() {
239 let node = MarketHierarchyNode::new(
240 "test_id".to_string(),
241 "Test Node".to_string(),
242 Some("parent_id".to_string()),
243 "IG".to_string(),
244 1,
245 "/Test Node".to_string(),
246 );
247
248 assert_eq!(node.id, "test_id");
249 assert_eq!(node.name, "Test Node");
250 assert_eq!(node.parent_id, Some("parent_id".to_string()));
251 assert_eq!(node.exchange, "IG");
252 assert_eq!(node.level, 1);
253 assert_eq!(node.path, "/Test Node");
254 }
255
256 #[test]
257 fn test_market_instrument_creation() {
258 let mut instrument = MarketInstrument::new(
259 "IX.D.DAX.DAILY.IP".to_string(),
260 "Germany 40".to_string(),
261 "INDICES".to_string(),
262 "node_123".to_string(),
263 "IG".to_string(),
264 );
265
266 assert_eq!(instrument.epic, "IX.D.DAX.DAILY.IP");
267 assert_eq!(instrument.instrument_name, "Germany 40");
268 assert_eq!(instrument.instrument_type, "INDICES");
269 assert_eq!(instrument.node_id, "node_123");
270 assert_eq!(instrument.exchange, "IG");
271
272 instrument.update_time = Some("2023-12-01T10:30:00Z".to_string());
274 instrument.parse_update_time_utc();
275 assert!(instrument.update_time_utc.is_some());
276 }
277}