1use crate::prelude::{MarketData, MarketNode};
2use crate::storage::market_persistence::{MarketHierarchyNode, MarketInstrument};
3use chrono::{DateTime, Utc};
4use sqlx::{AssertSqlSafe, Executor, PgPool, Row};
5use std::collections::HashMap;
6use std::future::Future;
7use tracing::info;
8
9pub struct MarketDatabaseService {
11 pool: PgPool,
12 exchange_name: String,
13}
14
15impl MarketDatabaseService {
16 pub fn new(pool: PgPool, exchange_name: String) -> Self {
18 Self {
19 pool,
20 exchange_name,
21 }
22 }
23
24 pub async fn initialize_database(&self) -> Result<(), sqlx::Error> {
26 info!("Initializing market database tables...");
27
28 sqlx::query(
30 r#"
31 CREATE TABLE IF NOT EXISTS market_hierarchy_nodes (
32 id VARCHAR(255) PRIMARY KEY,
33 name VARCHAR(500) NOT NULL,
34 parent_id VARCHAR(255) REFERENCES market_hierarchy_nodes(id),
35 exchange VARCHAR(50) NOT NULL,
36 level INTEGER NOT NULL DEFAULT 0,
37 path TEXT NOT NULL,
38 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
39 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
40 )
41 "#,
42 )
43 .execute(&self.pool)
44 .await?;
45
46 sqlx::query(
48 r#"
49 CREATE TABLE IF NOT EXISTS market_instruments (
50 epic VARCHAR(255) PRIMARY KEY,
51 instrument_name VARCHAR(500) NOT NULL,
52 instrument_type VARCHAR(100) NOT NULL,
53 node_id VARCHAR(255) NOT NULL REFERENCES market_hierarchy_nodes(id),
54 exchange VARCHAR(50) NOT NULL,
55 expiry VARCHAR(50) NOT NULL DEFAULT '',
56 high_limit_price DOUBLE PRECISION,
57 low_limit_price DOUBLE PRECISION,
58 market_status VARCHAR(50) NOT NULL,
59 net_change DOUBLE PRECISION,
60 percentage_change DOUBLE PRECISION,
61 update_time VARCHAR(50),
62 update_time_utc TIMESTAMPTZ,
63 bid DOUBLE PRECISION,
64 offer DOUBLE PRECISION,
65 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
66 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
67 )
68 "#,
69 )
70 .execute(&self.pool)
71 .await?;
72
73 let hierarchy_indexes = [
75 "CREATE INDEX IF NOT EXISTS idx_market_hierarchy_parent_id ON market_hierarchy_nodes(parent_id)",
76 "CREATE INDEX IF NOT EXISTS idx_market_hierarchy_exchange ON market_hierarchy_nodes(exchange)",
77 "CREATE INDEX IF NOT EXISTS idx_market_hierarchy_level ON market_hierarchy_nodes(level)",
78 "CREATE INDEX IF NOT EXISTS idx_market_hierarchy_path ON market_hierarchy_nodes USING gin(to_tsvector('english', path))",
79 "CREATE INDEX IF NOT EXISTS idx_market_hierarchy_name ON market_hierarchy_nodes USING gin(to_tsvector('english', name))",
80 ];
81
82 for index_sql in hierarchy_indexes {
83 sqlx::query(index_sql).execute(&self.pool).await?;
84 }
85
86 let instrument_indexes = [
88 "CREATE INDEX IF NOT EXISTS idx_market_instruments_node_id ON market_instruments(node_id)",
89 "CREATE INDEX IF NOT EXISTS idx_market_instruments_exchange ON market_instruments(exchange)",
90 "CREATE INDEX IF NOT EXISTS idx_market_instruments_type ON market_instruments(instrument_type)",
91 "CREATE INDEX IF NOT EXISTS idx_market_instruments_status ON market_instruments(market_status)",
92 "CREATE INDEX IF NOT EXISTS idx_market_instruments_name ON market_instruments USING gin(to_tsvector('english', instrument_name))",
93 "CREATE INDEX IF NOT EXISTS idx_market_instruments_epic ON market_instruments(epic)",
94 "CREATE INDEX IF NOT EXISTS idx_market_instruments_expiry ON market_instruments(expiry)",
95 ];
96
97 for index_sql in instrument_indexes {
98 sqlx::query(index_sql).execute(&self.pool).await?;
99 }
100
101 sqlx::query(
103 r#"
104 CREATE OR REPLACE FUNCTION update_updated_at_column()
105 RETURNS TRIGGER AS $$
106 BEGIN
107 NEW.updated_at = NOW();
108 RETURN NEW;
109 END;
110 $$ language 'plpgsql'
111 "#,
112 )
113 .execute(&self.pool)
114 .await?;
115
116 sqlx::query("DROP TRIGGER IF EXISTS update_market_hierarchy_nodes_updated_at ON market_hierarchy_nodes")
118 .execute(&self.pool)
119 .await?;
120
121 sqlx::query(
122 r#"
123 CREATE TRIGGER update_market_hierarchy_nodes_updated_at
124 BEFORE UPDATE ON market_hierarchy_nodes
125 FOR EACH ROW
126 EXECUTE FUNCTION update_updated_at_column()
127 "#,
128 )
129 .execute(&self.pool)
130 .await?;
131
132 sqlx::query(
133 "DROP TRIGGER IF EXISTS update_market_instruments_updated_at ON market_instruments",
134 )
135 .execute(&self.pool)
136 .await?;
137
138 sqlx::query(
139 r#"
140 CREATE TRIGGER update_market_instruments_updated_at
141 BEFORE UPDATE ON market_instruments
142 FOR EACH ROW
143 EXECUTE FUNCTION update_updated_at_column()
144 "#,
145 )
146 .execute(&self.pool)
147 .await?;
148
149 info!("Market database tables initialized successfully");
150 Ok(())
151 }
152
153 pub async fn store_market_hierarchy(
155 &self,
156 hierarchy: &[MarketNode],
157 ) -> Result<(), sqlx::Error> {
158 info!(
159 "Storing market hierarchy with {} top-level nodes",
160 hierarchy.len()
161 );
162
163 let mut tx = self.pool.begin().await?;
165
166 sqlx::query("DELETE FROM market_instruments WHERE exchange = $1")
168 .bind(&self.exchange_name)
169 .execute(&mut *tx)
170 .await?;
171
172 sqlx::query("DELETE FROM market_hierarchy_nodes WHERE exchange = $1")
173 .bind(&self.exchange_name)
174 .execute(&mut *tx)
175 .await?;
176
177 let mut node_count = 0;
179 let mut instrument_count = 0;
180
181 for node in hierarchy {
182 let (nodes, instruments) = self.process_node_recursive(node, None, 0, "").await?;
183 node_count += nodes.len();
184 instrument_count += instruments.len();
185
186 for node in nodes {
188 self.insert_hierarchy_node(&mut tx, &node).await?;
189 }
190
191 for instrument in instruments {
193 self.insert_market_instrument(&mut tx, &instrument).await?;
194 }
195 }
196
197 tx.commit().await?;
199
200 info!(
201 "Successfully stored {} hierarchy nodes and {} instruments",
202 node_count, instrument_count
203 );
204 Ok(())
205 }
206
207 pub async fn store_filtered_market_nodes(
211 &self,
212 hierarchy: &[MarketNode],
213 symbol_map: &HashMap<&str, &str>,
214 table_name: &str,
215 ) -> Result<(), sqlx::Error> {
216 info!(
217 "Storing filtered market nodes to table '{}' with {} top-level nodes",
218 table_name,
219 hierarchy.len()
220 );
221
222 let mut tx = self.pool.begin().await?;
224
225 let create_table_sql = format!(
227 r#"
228 CREATE TABLE IF NOT EXISTS {} (
229 epic VARCHAR(255) PRIMARY KEY,
230 instrumentName TEXT NOT NULL,
231 instrumentType VARCHAR(50) NOT NULL,
232 expiry VARCHAR(50),
233 lastUpdateUTC TIMESTAMP,
234 symbol VARCHAR(50)
235 )
236 "#,
237 table_name
238 );
239
240 tx.execute(sqlx::query(AssertSqlSafe(create_table_sql)))
243 .await?;
244
245 let mut inserted_count = 0;
248
249 for node in hierarchy {
251 inserted_count += self
252 .process_filtered_node_recursive(node, symbol_map, table_name, &mut tx)
253 .await?;
254 }
255
256 tx.commit().await?;
258
259 info!(
260 "Successfully stored {} filtered instruments in table '{}'",
261 inserted_count, table_name
262 );
263 Ok(())
264 }
265
266 fn process_filtered_node_recursive<'a>(
268 &'a self,
269 node: &'a MarketNode,
270 symbol_map: &'a HashMap<&str, &str>,
271 table_name: &'a str,
272 tx: &'a mut sqlx::Transaction<'_, sqlx::Postgres>,
273 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<i32, sqlx::Error>> + 'a>> {
274 Box::pin(async move {
275 let mut count = 0;
276
277 for market in &node.markets {
279 if self.is_valid_epic_format(&market.epic) {
280 let symbol = self.find_symbol_for_market(&market.instrument_name, symbol_map);
281 self.insert_filtered_market(market, &symbol, table_name, tx)
282 .await?;
283 count += 1;
284 }
285 }
286
287 for child in &node.children {
289 count += self
290 .process_filtered_node_recursive(child, symbol_map, table_name, tx)
291 .await?;
292 }
293
294 Ok(count)
295 })
296 }
297
298 pub fn is_valid_epic_format(&self, epic: &str) -> bool {
300 epic.matches('.').count() == 4
301 }
302
303 pub fn find_symbol_for_market(
305 &self,
306 instrument_name: &str,
307 symbol_map: &HashMap<&str, &str>,
308 ) -> String {
309 let name_lower = instrument_name.to_lowercase();
310
311 for (key, value) in symbol_map {
312 if name_lower.contains(&key.to_lowercase()) {
313 return value.to_string();
314 }
315 }
316
317 "UNKNOWN".to_string()
319 }
320
321 pub fn convert_update_time(&self, update_time: &Option<String>) -> Option<DateTime<Utc>> {
323 if let Some(time_str) = update_time
324 && let Ok(timestamp_ms) = time_str.parse::<i64>()
325 {
326 let timestamp_secs = timestamp_ms / 1000;
327 let nanosecs = ((timestamp_ms % 1000) * 1_000_000) as u32;
328
329 return DateTime::from_timestamp(timestamp_secs, nanosecs);
330 }
331 None
332 }
333
334 async fn insert_filtered_market(
336 &self,
337 market: &MarketData,
338 symbol: &str,
339 table_name: &str,
340 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
341 ) -> Result<(), sqlx::Error> {
342 let last_update_utc = self.convert_update_time(&market.update_time);
343
344 let insert_sql = format!(
345 r#"
346 INSERT INTO {} (epic, instrumentName, instrumentType, expiry, lastUpdateUTC, symbol)
347 VALUES ($1, $2, $3, $4, $5, $6)
348 ON CONFLICT (epic) DO UPDATE SET
349 instrumentName = EXCLUDED.instrumentName,
350 instrumentType = EXCLUDED.instrumentType,
351 expiry = EXCLUDED.expiry,
352 lastUpdateUTC = EXCLUDED.lastUpdateUTC,
353 symbol = EXCLUDED.symbol
354 "#,
355 table_name
356 );
357
358 tx.execute(
359 sqlx::query(AssertSqlSafe(insert_sql))
361 .bind(&market.epic)
362 .bind(&market.instrument_name)
363 .bind(format!("{:?}", market.instrument_type))
364 .bind(&market.expiry)
365 .bind(last_update_utc)
366 .bind(symbol),
367 )
368 .await?;
369
370 Ok(())
371 }
372
373 #[allow(clippy::type_complexity)]
375 fn process_node_recursive<'a>(
376 &'a self,
377 node: &'a MarketNode,
378 parent_id: Option<&'a str>,
379 level: i32,
380 parent_path: &'a str,
381 ) -> std::pin::Pin<
382 Box<
383 dyn Future<
384 Output = Result<(Vec<MarketHierarchyNode>, Vec<MarketInstrument>), sqlx::Error>,
385 > + 'a,
386 >,
387 > {
388 Box::pin(async move {
389 let mut all_nodes = Vec::new();
390 let mut all_instruments = Vec::new();
391
392 let current_path = MarketHierarchyNode::build_path(
394 if parent_path.is_empty() {
395 None
396 } else {
397 Some(parent_path)
398 },
399 &node.name,
400 );
401
402 let current_node = MarketHierarchyNode::new(
404 node.id.clone(),
405 node.name.clone(),
406 parent_id.map(|s| s.to_string()),
407 self.exchange_name.clone(),
408 level,
409 current_path.clone(),
410 );
411
412 all_nodes.push(current_node);
413
414 for market in &node.markets {
416 let mut instrument = self.convert_market_data_to_instrument(market, &node.id);
417 instrument.parse_update_time_utc();
418 all_instruments.push(instrument);
419 }
420
421 for child in &node.children {
423 let (child_nodes, child_instruments) = self
424 .process_node_recursive(child, Some(&node.id), level + 1, ¤t_path)
425 .await?;
426 all_nodes.extend(child_nodes);
427 all_instruments.extend(child_instruments);
428 }
429
430 Ok((all_nodes, all_instruments))
431 })
432 }
433
434 pub fn convert_market_data_to_instrument(
436 &self,
437 market: &MarketData,
438 node_id: &str,
439 ) -> MarketInstrument {
440 let mut instrument = MarketInstrument::new(
441 market.epic.clone(),
442 market.instrument_name.clone(),
443 format!("{:?}", market.instrument_type).to_uppercase(),
444 node_id.to_string(),
445 self.exchange_name.clone(),
446 );
447
448 instrument.expiry = market.expiry.clone();
449 instrument.high_limit_price = market.high_limit_price;
450 instrument.low_limit_price = market.low_limit_price;
451 instrument.market_status = market.market_status.clone();
452 instrument.net_change = market.net_change;
453 instrument.percentage_change = market.percentage_change;
454 instrument.update_time = market.update_time.clone();
455 instrument.bid = market.bid;
456 instrument.offer = market.offer;
457
458 instrument
459 }
460
461 async fn insert_hierarchy_node(
463 &self,
464 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
465 node: &MarketHierarchyNode,
466 ) -> Result<(), sqlx::Error> {
467 tx.execute(
468 sqlx::query(
469 r#"
470 INSERT INTO market_hierarchy_nodes
471 (id, name, parent_id, exchange, level, path, created_at, updated_at)
472 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
473 ON CONFLICT (id) DO UPDATE SET
474 name = EXCLUDED.name,
475 parent_id = EXCLUDED.parent_id,
476 exchange = EXCLUDED.exchange,
477 level = EXCLUDED.level,
478 path = EXCLUDED.path,
479 updated_at = EXCLUDED.updated_at
480 "#,
481 )
482 .bind(&node.id)
483 .bind(&node.name)
484 .bind(&node.parent_id)
485 .bind(&node.exchange)
486 .bind(node.level)
487 .bind(&node.path)
488 .bind(node.created_at)
489 .bind(node.updated_at),
490 )
491 .await?;
492
493 Ok(())
494 }
495
496 async fn insert_market_instrument(
498 &self,
499 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
500 instrument: &MarketInstrument,
501 ) -> Result<(), sqlx::Error> {
502 tx.execute(
503 sqlx::query(
504 r#"
505 INSERT INTO market_instruments
506 (epic, instrument_name, instrument_type, node_id, exchange, expiry,
507 high_limit_price, low_limit_price, market_status, net_change,
508 percentage_change, update_time, update_time_utc, bid, offer,
509 created_at, updated_at)
510 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
511 ON CONFLICT (epic) DO UPDATE SET
512 instrument_name = EXCLUDED.instrument_name,
513 instrument_type = EXCLUDED.instrument_type,
514 node_id = EXCLUDED.node_id,
515 exchange = EXCLUDED.exchange,
516 expiry = EXCLUDED.expiry,
517 high_limit_price = EXCLUDED.high_limit_price,
518 low_limit_price = EXCLUDED.low_limit_price,
519 market_status = EXCLUDED.market_status,
520 net_change = EXCLUDED.net_change,
521 percentage_change = EXCLUDED.percentage_change,
522 update_time = EXCLUDED.update_time,
523 update_time_utc = EXCLUDED.update_time_utc,
524 bid = EXCLUDED.bid,
525 offer = EXCLUDED.offer,
526 updated_at = EXCLUDED.updated_at
527 "#,
528 )
529 .bind(&instrument.epic)
530 .bind(&instrument.instrument_name)
531 .bind(&instrument.instrument_type)
532 .bind(&instrument.node_id)
533 .bind(&instrument.exchange)
534 .bind(&instrument.expiry)
535 .bind(instrument.high_limit_price)
536 .bind(instrument.low_limit_price)
537 .bind(&instrument.market_status)
538 .bind(instrument.net_change)
539 .bind(instrument.percentage_change)
540 .bind(&instrument.update_time)
541 .bind(instrument.update_time_utc)
542 .bind(instrument.bid)
543 .bind(instrument.offer)
544 .bind(instrument.created_at)
545 .bind(instrument.updated_at),
546 )
547 .await?;
548
549 Ok(())
550 }
551
552 pub async fn get_market_hierarchy(&self) -> Result<Vec<MarketHierarchyNode>, sqlx::Error> {
554 let nodes = sqlx::query_as::<_, MarketHierarchyNode>(
555 "SELECT * FROM market_hierarchy_nodes WHERE exchange = $1 ORDER BY level, name",
556 )
557 .bind(&self.exchange_name)
558 .fetch_all(&self.pool)
559 .await?;
560
561 Ok(nodes)
562 }
563
564 pub async fn get_instruments_by_node(
566 &self,
567 node_id: &str,
568 ) -> Result<Vec<MarketInstrument>, sqlx::Error> {
569 let instruments = sqlx::query_as::<_, MarketInstrument>(
570 "SELECT * FROM market_instruments WHERE node_id = $1 AND exchange = $2 ORDER BY instrument_name",
571 )
572 .bind(node_id)
573 .bind(&self.exchange_name)
574 .fetch_all(&self.pool)
575 .await?;
576
577 Ok(instruments)
578 }
579
580 pub async fn search_instruments(
582 &self,
583 search_term: &str,
584 ) -> Result<Vec<MarketInstrument>, sqlx::Error> {
585 let instruments = sqlx::query_as::<_, MarketInstrument>(
586 r#"
587 SELECT * FROM market_instruments
588 WHERE exchange = $1
589 AND (
590 instrument_name ILIKE $2
591 OR epic ILIKE $2
592 OR to_tsvector('english', instrument_name) @@ plainto_tsquery('english', $3)
593 )
594 ORDER BY instrument_name
595 LIMIT 100
596 "#,
597 )
598 .bind(&self.exchange_name)
599 .bind(format!("%{search_term}%"))
600 .bind(search_term)
601 .fetch_all(&self.pool)
602 .await?;
603
604 Ok(instruments)
605 }
606
607 pub async fn get_statistics(&self) -> Result<DatabaseStatistics, sqlx::Error> {
609 let node_count: i64 =
610 sqlx::query_scalar("SELECT COUNT(*) FROM market_hierarchy_nodes WHERE exchange = $1")
611 .bind(&self.exchange_name)
612 .fetch_one(&self.pool)
613 .await?;
614
615 let instrument_count: i64 =
616 sqlx::query_scalar("SELECT COUNT(*) FROM market_instruments WHERE exchange = $1")
617 .bind(&self.exchange_name)
618 .fetch_one(&self.pool)
619 .await?;
620
621 let instrument_types: Vec<(String, i64)> = sqlx::query(
622 "SELECT instrument_type, COUNT(*) as count FROM market_instruments WHERE exchange = $1 GROUP BY instrument_type ORDER BY count DESC",
623 )
624 .bind(&self.exchange_name)
625 .fetch_all(&self.pool)
626 .await?
627 .into_iter()
628 .map(|row| (row.get::<String, _>("instrument_type"), row.get::<i64, _>("count")))
629 .collect();
630
631 let max_depth: i32 = sqlx::query_scalar(
632 "SELECT COALESCE(MAX(level), 0) FROM market_hierarchy_nodes WHERE exchange = $1",
633 )
634 .bind(&self.exchange_name)
635 .fetch_one(&self.pool)
636 .await?;
637
638 Ok(DatabaseStatistics {
639 exchange: self.exchange_name.clone(),
640 node_count,
641 instrument_count,
642 instrument_types,
643 max_hierarchy_depth: max_depth,
644 })
645 }
646}
647
648#[derive(Debug, Clone)]
650pub struct DatabaseStatistics {
651 pub exchange: String,
653 pub node_count: i64,
655 pub instrument_count: i64,
657 pub instrument_types: Vec<(String, i64)>,
659 pub max_hierarchy_depth: i32,
661}
662
663impl DatabaseStatistics {
664 pub fn print_summary(&self) {
666 info!("=== Market Database Statistics for {} ===", self.exchange);
667 info!("Hierarchy nodes: {}", self.node_count);
668 info!("Market instruments: {}", self.instrument_count);
669 info!("Maximum hierarchy depth: {}", self.max_hierarchy_depth);
670 info!("Instrument types:");
671 for (instrument_type, count) in &self.instrument_types {
672 info!(" {}: {}", instrument_type, count);
673 }
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::presentation::instrument::InstrumentType;
681
682 #[tokio::test]
683 #[ignore]
684 async fn test_convert_market_data_to_instrument() {
685 let service = MarketDatabaseService::new(
686 PgPool::connect("postgresql://test")
688 .await
689 .unwrap_or_else(|_| panic!("Test requires a PostgreSQL connection")),
690 "IG".to_string(),
691 );
692
693 let market_data = MarketData {
694 epic: "IX.D.DAX.DAILY.IP".to_string(),
695 instrument_name: "Germany 40".to_string(),
696 instrument_type: InstrumentType::Indices,
697 expiry: "DFB".to_string(),
698 high_limit_price: Some(20000.0),
699 low_limit_price: Some(5000.0),
700 market_status: "TRADEABLE".to_string(),
701 net_change: Some(100.5),
702 percentage_change: Some(0.65),
703 update_time: Some("2023-12-01T10:30:00".to_string()),
704 update_time_utc: Some("2023-12-01T10:30:00Z".to_string()),
705 bid: Some(15450.2),
706 offer: Some(15451.8),
707 };
708
709 let instrument = service.convert_market_data_to_instrument(&market_data, "node_123");
710
711 assert_eq!(instrument.epic, "IX.D.DAX.DAILY.IP");
712 assert_eq!(instrument.instrument_name, "Germany 40");
713 assert_eq!(instrument.instrument_type, "INDICES");
714 assert_eq!(instrument.node_id, "node_123");
715 assert_eq!(instrument.exchange, "IG");
716 assert_eq!(instrument.expiry, "DFB");
717 assert_eq!(instrument.high_limit_price, Some(20000.0));
718 assert_eq!(instrument.bid, Some(15450.2));
719 assert_eq!(instrument.offer, Some(15451.8));
720 }
721}