Skip to main content

ig_client/storage/
market_persistence.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4
5/// Represents a market hierarchy node in the database
6/// This structure is optimized for PostgreSQL storage with proper indexing
7#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
8pub struct MarketHierarchyNode {
9    /// Unique identifier for the node
10    pub id: String,
11    /// Human-readable name of the node
12    pub name: String,
13    /// Parent node ID (NULL for root nodes)
14    pub parent_id: Option<String>,
15    /// Exchange name (e.g., "IG")
16    pub exchange: String,
17    /// Depth level in the hierarchy (0 for root nodes)
18    pub level: i32,
19    /// Full path from root to this node (e.g., "/Indices/Europe/Germany")
20    pub path: String,
21    /// Timestamp when this record was created
22    pub created_at: DateTime<Utc>,
23    /// Timestamp when this record was last updated
24    pub updated_at: DateTime<Utc>,
25}
26
27/// Represents a market instrument in the database
28/// This structure is optimized for PostgreSQL storage with proper indexing
29#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
30pub struct MarketInstrument {
31    /// Unique identifier for the market (epic)
32    pub epic: String,
33    /// Human-readable name of the instrument
34    pub instrument_name: String,
35    /// Type of the instrument (e.g., "SHARES", "INDICES", "CURRENCIES")
36    pub instrument_type: String,
37    /// Node ID where this instrument belongs
38    pub node_id: String,
39    /// Exchange name (e.g., "IG")
40    pub exchange: String,
41    /// Expiry date of the instrument (empty string for perpetual instruments)
42    pub expiry: String,
43    /// Upper price limit for the market
44    pub high_limit_price: Option<f64>,
45    /// Lower price limit for the market
46    pub low_limit_price: Option<f64>,
47    /// Current status of the market
48    pub market_status: String,
49    /// Net change in price since previous close
50    pub net_change: Option<f64>,
51    /// Percentage change in price since previous close
52    pub percentage_change: Option<f64>,
53    /// Time of the last price update
54    pub update_time: Option<String>,
55    /// Time of the last price update in UTC
56    pub update_time_utc: Option<DateTime<Utc>>,
57    /// Current bid price
58    pub bid: Option<f64>,
59    /// Current offer/ask price
60    pub offer: Option<f64>,
61    /// Timestamp when this record was created
62    pub created_at: DateTime<Utc>,
63    /// Timestamp when this record was last updated
64    pub updated_at: DateTime<Utc>,
65}
66
67impl MarketInstrument {
68    /// Checks if the current financial instrument is a call option.
69    ///
70    /// A call option is a financial derivative that gives the holder the right (but not the obligation)
71    /// to buy an underlying asset at a specified price within a specified time period. This method checks
72    /// whether the instrument represented by this instance is a call option by inspecting the `instrument_name`
73    /// field.
74    ///
75    /// # Returns
76    ///
77    /// * `true` if the instrument's name contains the substring `"CALL"`, indicating it is a call option.
78    /// * `false` otherwise.
79    ///
80    pub fn is_call(&self) -> bool {
81        self.instrument_name.contains("CALL")
82    }
83
84    /// Checks if the financial instrument is a "PUT" option.
85    ///
86    /// This method examines the `instrument_name` field of the struct to determine
87    /// if it contains the substring "PUT". If the substring is found, the method
88    /// returns `true`, indicating that the instrument is categorized as a "PUT" option.
89    /// Otherwise, it returns `false`.
90    ///
91    /// # Returns
92    /// * `true` - If `instrument_name` contains the substring "PUT".
93    /// * `false` - If `instrument_name` does not contain the substring "PUT".
94    ///
95    pub fn is_put(&self) -> bool {
96        self.instrument_name.contains("PUT")
97    }
98}
99
100impl MarketHierarchyNode {
101    /// Creates a new MarketHierarchyNode
102    pub fn new(
103        id: String,
104        name: String,
105        parent_id: Option<String>,
106        exchange: String,
107        level: i32,
108        path: String,
109    ) -> Self {
110        let now = Utc::now();
111        Self {
112            id,
113            name,
114            parent_id,
115            exchange,
116            level,
117            path,
118            created_at: now,
119            updated_at: now,
120        }
121    }
122
123    /// Builds the full path for a node based on its parent path
124    pub fn build_path(parent_path: Option<&str>, node_name: &str) -> String {
125        match parent_path {
126            Some(parent) if !parent.is_empty() => format!("{parent}/{node_name}"),
127            _ => format!("/{node_name}"),
128        }
129    }
130}
131
132impl MarketInstrument {
133    /// Creates a new MarketInstrument
134    pub fn new(
135        epic: String,
136        instrument_name: String,
137        instrument_type: String,
138        node_id: String,
139        exchange: String,
140    ) -> Self {
141        let now = Utc::now();
142        Self {
143            epic,
144            instrument_name,
145            instrument_type,
146            node_id,
147            exchange,
148            expiry: String::new(),
149            high_limit_price: None,
150            low_limit_price: None,
151            market_status: String::new(),
152            net_change: None,
153            percentage_change: None,
154            update_time: None,
155            update_time_utc: None,
156            bid: None,
157            offer: None,
158            created_at: now,
159            updated_at: now,
160        }
161    }
162
163    /// Parses the update_time_utc from a string if available
164    pub fn parse_update_time_utc(&mut self) {
165        if let Some(ref time_str) = self.update_time
166            && let Ok(parsed_time) = DateTime::parse_from_rfc3339(time_str)
167        {
168            self.update_time_utc = Some(parsed_time.with_timezone(&Utc));
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_build_path() {
179        assert_eq!(MarketHierarchyNode::build_path(None, "Root"), "/Root");
180        assert_eq!(
181            MarketHierarchyNode::build_path(Some("/Root"), "Child"),
182            "/Root/Child"
183        );
184        assert_eq!(
185            MarketHierarchyNode::build_path(Some("/Root/Child"), "Grandchild"),
186            "/Root/Child/Grandchild"
187        );
188    }
189
190    #[test]
191    fn test_market_hierarchy_node_creation() {
192        let node = MarketHierarchyNode::new(
193            "test_id".to_string(),
194            "Test Node".to_string(),
195            Some("parent_id".to_string()),
196            "IG".to_string(),
197            1,
198            "/Test Node".to_string(),
199        );
200
201        assert_eq!(node.id, "test_id");
202        assert_eq!(node.name, "Test Node");
203        assert_eq!(node.parent_id, Some("parent_id".to_string()));
204        assert_eq!(node.exchange, "IG");
205        assert_eq!(node.level, 1);
206        assert_eq!(node.path, "/Test Node");
207    }
208
209    #[test]
210    fn test_market_instrument_creation() {
211        let mut instrument = MarketInstrument::new(
212            "IX.D.DAX.DAILY.IP".to_string(),
213            "Germany 40".to_string(),
214            "INDICES".to_string(),
215            "node_123".to_string(),
216            "IG".to_string(),
217        );
218
219        assert_eq!(instrument.epic, "IX.D.DAX.DAILY.IP");
220        assert_eq!(instrument.instrument_name, "Germany 40");
221        assert_eq!(instrument.instrument_type, "INDICES");
222        assert_eq!(instrument.node_id, "node_123");
223        assert_eq!(instrument.exchange, "IG");
224
225        // Test update_time_utc parsing
226        instrument.update_time = Some("2023-12-01T10:30:00Z".to_string());
227        instrument.parse_update_time_utc();
228        assert!(instrument.update_time_utc.is_some());
229    }
230}