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
67impl MarketInstrument {
68 pub fn is_call(&self) -> bool {
81 self.instrument_name.contains("CALL")
82 }
83
84 pub fn is_put(&self) -> bool {
96 self.instrument_name.contains("PUT")
97 }
98}
99
100pub const CREATE_MARKET_HIERARCHY_TABLE: &str = r#"
102CREATE TABLE IF NOT EXISTS market_hierarchy_nodes (
103 id VARCHAR(255) PRIMARY KEY,
104 name VARCHAR(500) NOT NULL,
105 parent_id VARCHAR(255) REFERENCES market_hierarchy_nodes(id),
106 exchange VARCHAR(50) NOT NULL,
107 level INTEGER NOT NULL DEFAULT 0,
108 path TEXT NOT NULL,
109 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
110 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
111);
112
113-- Indexes for efficient querying
114CREATE INDEX IF NOT EXISTS idx_market_hierarchy_parent_id ON market_hierarchy_nodes(parent_id);
115CREATE INDEX IF NOT EXISTS idx_market_hierarchy_exchange ON market_hierarchy_nodes(exchange);
116CREATE INDEX IF NOT EXISTS idx_market_hierarchy_level ON market_hierarchy_nodes(level);
117CREATE INDEX IF NOT EXISTS idx_market_hierarchy_path ON market_hierarchy_nodes USING gin(to_tsvector('english', path));
118CREATE INDEX IF NOT EXISTS idx_market_hierarchy_name ON market_hierarchy_nodes USING gin(to_tsvector('english', name));
119"#;
120
121pub const CREATE_MARKET_INSTRUMENTS_TABLE: &str = r#"
125CREATE TABLE IF NOT EXISTS market_instruments (
126 epic VARCHAR(255) PRIMARY KEY,
127 instrument_name VARCHAR(500) NOT NULL,
128 instrument_type VARCHAR(100) NOT NULL,
129 node_id VARCHAR(255) NOT NULL REFERENCES market_hierarchy_nodes(id),
130 exchange VARCHAR(50) NOT NULL,
131 expiry VARCHAR(50) NOT NULL DEFAULT '',
132 high_limit_price DOUBLE PRECISION,
133 low_limit_price DOUBLE PRECISION,
134 market_status VARCHAR(50) NOT NULL,
135 net_change DOUBLE PRECISION,
136 percentage_change DOUBLE PRECISION,
137 update_time VARCHAR(50),
138 update_time_utc TIMESTAMPTZ,
139 bid DOUBLE PRECISION,
140 offer DOUBLE PRECISION,
141 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
142 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
143);
144
145-- Indexes for efficient querying
146CREATE INDEX IF NOT EXISTS idx_market_instruments_node_id ON market_instruments(node_id);
147CREATE INDEX IF NOT EXISTS idx_market_instruments_exchange ON market_instruments(exchange);
148CREATE INDEX IF NOT EXISTS idx_market_instruments_type ON market_instruments(instrument_type);
149CREATE INDEX IF NOT EXISTS idx_market_instruments_status ON market_instruments(market_status);
150CREATE INDEX IF NOT EXISTS idx_market_instruments_name ON market_instruments USING gin(to_tsvector('english', instrument_name));
151CREATE INDEX IF NOT EXISTS idx_market_instruments_epic ON market_instruments(epic);
152CREATE INDEX IF NOT EXISTS idx_market_instruments_expiry ON market_instruments(expiry);
153"#;
154
155pub const CREATE_UPDATE_TIMESTAMP_TRIGGER: &str = r#"
157-- Function to update the updated_at timestamp
158CREATE OR REPLACE FUNCTION update_updated_at_column()
159RETURNS TRIGGER AS $$
160BEGIN
161 NEW.updated_at = NOW();
162 RETURN NEW;
163END;
164$$ language 'plpgsql';
165
166-- Triggers for both tables
167DROP TRIGGER IF EXISTS update_market_hierarchy_nodes_updated_at ON market_hierarchy_nodes;
168CREATE TRIGGER update_market_hierarchy_nodes_updated_at
169 BEFORE UPDATE ON market_hierarchy_nodes
170 FOR EACH ROW
171 EXECUTE FUNCTION update_updated_at_column();
172
173DROP TRIGGER IF EXISTS update_market_instruments_updated_at ON market_instruments;
174CREATE TRIGGER update_market_instruments_updated_at
175 BEFORE UPDATE ON market_instruments
176 FOR EACH ROW
177 EXECUTE FUNCTION update_updated_at_column();
178"#;
179
180impl MarketHierarchyNode {
181 pub fn new(
183 id: String,
184 name: String,
185 parent_id: Option<String>,
186 exchange: String,
187 level: i32,
188 path: String,
189 ) -> Self {
190 let now = Utc::now();
191 Self {
192 id,
193 name,
194 parent_id,
195 exchange,
196 level,
197 path,
198 created_at: now,
199 updated_at: now,
200 }
201 }
202
203 pub fn build_path(parent_path: Option<&str>, node_name: &str) -> String {
205 match parent_path {
206 Some(parent) if !parent.is_empty() => format!("{parent}/{node_name}"),
207 _ => format!("/{node_name}"),
208 }
209 }
210}
211
212impl MarketInstrument {
213 pub fn new(
215 epic: String,
216 instrument_name: String,
217 instrument_type: String,
218 node_id: String,
219 exchange: String,
220 ) -> Self {
221 let now = Utc::now();
222 Self {
223 epic,
224 instrument_name,
225 instrument_type,
226 node_id,
227 exchange,
228 expiry: String::new(),
229 high_limit_price: None,
230 low_limit_price: None,
231 market_status: String::new(),
232 net_change: None,
233 percentage_change: None,
234 update_time: None,
235 update_time_utc: None,
236 bid: None,
237 offer: None,
238 created_at: now,
239 updated_at: now,
240 }
241 }
242
243 pub fn parse_update_time_utc(&mut self) {
245 if let Some(ref time_str) = self.update_time
246 && let Ok(parsed_time) = DateTime::parse_from_rfc3339(time_str)
247 {
248 self.update_time_utc = Some(parsed_time.with_timezone(&Utc));
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_build_path() {
259 assert_eq!(MarketHierarchyNode::build_path(None, "Root"), "/Root");
260 assert_eq!(
261 MarketHierarchyNode::build_path(Some("/Root"), "Child"),
262 "/Root/Child"
263 );
264 assert_eq!(
265 MarketHierarchyNode::build_path(Some("/Root/Child"), "Grandchild"),
266 "/Root/Child/Grandchild"
267 );
268 }
269
270 #[test]
271 fn test_market_hierarchy_node_creation() {
272 let node = MarketHierarchyNode::new(
273 "test_id".to_string(),
274 "Test Node".to_string(),
275 Some("parent_id".to_string()),
276 "IG".to_string(),
277 1,
278 "/Test Node".to_string(),
279 );
280
281 assert_eq!(node.id, "test_id");
282 assert_eq!(node.name, "Test Node");
283 assert_eq!(node.parent_id, Some("parent_id".to_string()));
284 assert_eq!(node.exchange, "IG");
285 assert_eq!(node.level, 1);
286 assert_eq!(node.path, "/Test Node");
287 }
288
289 #[test]
290 fn test_market_instrument_creation() {
291 let mut instrument = MarketInstrument::new(
292 "IX.D.DAX.DAILY.IP".to_string(),
293 "Germany 40".to_string(),
294 "INDICES".to_string(),
295 "node_123".to_string(),
296 "IG".to_string(),
297 );
298
299 assert_eq!(instrument.epic, "IX.D.DAX.DAILY.IP");
300 assert_eq!(instrument.instrument_name, "Germany 40");
301 assert_eq!(instrument.instrument_type, "INDICES");
302 assert_eq!(instrument.node_id, "node_123");
303 assert_eq!(instrument.exchange, "IG");
304
305 instrument.update_time = Some("2023-12-01T10:30:00Z".to_string());
307 instrument.parse_update_time_utc();
308 assert!(instrument.update_time_utc.is_some());
309 }
310}