1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use tracing::warn;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct ParsedOptionInfo {
9 pub asset_name: String,
11 pub strike: Option<String>,
13 pub option_type: Option<String>,
15}
16
17impl fmt::Display for ParsedOptionInfo {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(
20 f,
21 "Asset: {}, Strike: {}, Type: {}",
22 self.asset_name,
23 self.strike.as_deref().unwrap_or("N/A"),
24 self.option_type.as_deref().unwrap_or("N/A")
25 )
26 }
27}
28
29#[derive(Debug, Serialize)]
31pub struct ParsedMarketData {
32 pub epic: String,
34 pub instrument_name: String,
36 pub expiry: String,
38 pub asset_name: String,
40 pub strike: Option<String>,
42 pub option_type: Option<String>,
44}
45
46pub fn normalize_text(text: &str) -> String {
51 if text.contains("Japón") {
53 return text.replace("Japón", "Japan");
54 }
55
56 let mut result = String::with_capacity(text.len());
57 for c in text.chars() {
58 match c {
59 'á' | 'à' | 'ä' | 'â' | 'ã' => result.push('a'),
60 'é' | 'è' | 'ë' | 'ê' => result.push('e'),
61 'í' | 'ì' | 'ï' | 'î' => result.push('i'),
62 'ó' | 'ò' | 'ö' | 'ô' | 'õ' => result.push('o'),
63 'ú' | 'ù' | 'ü' | 'û' => result.push('u'),
64 'ñ' => result.push('n'),
65 'ç' => result.push('c'),
66 'Á' | 'À' | 'Ä' | 'Â' | 'Ã' => result.push('A'),
67 'É' | 'È' | 'Ë' | 'Ê' => result.push('E'),
68 'Í' | 'Ì' | 'Ï' | 'Î' => result.push('I'),
69 'Ó' | 'Ò' | 'Ö' | 'Ô' | 'Õ' => result.push('O'),
70 'Ú' | 'Ù' | 'Ü' | 'Û' => result.push('U'),
71 'Ñ' => result.push('N'),
72 'Ç' => result.push('C'),
73 _ => result.push(c),
74 }
75 }
76 result
77}
78
79pub fn parse_instrument_name(instrument_name: &str) -> ParsedOptionInfo {
97 lazy_static::lazy_static! {
100 static ref OPTION_PATTERN: Regex = Regex::new(r"^(.*?)\s+(\d+(?:\.\d+)?)\s+(CALL|PUT)(?:\s+\(.*?\))?$").unwrap();
102
103 static ref DECIMAL_OPTION_PATTERN: Regex = Regex::new(r"^(.*?)\s+(\d+\.\d+)\s+(CALL|PUT)(?:\s+\(.*?\))?$").unwrap();
105
106 static ref SPECIAL_OPTION_PATTERN: Regex = Regex::new(r"^(.*?)\s+\(([^)]+)\)(\d+)\s+(CALL|PUT)(?:\s+\(.*?\))?$").unwrap();
108
109 static ref INCOMPLETE_PAREN_PATTERN: Regex = Regex::new(r"^(.*?)\s+(\d+(?:\.\d+)?)\s+(CALL|PUT)\s+\([^)]*$").unwrap();
111
112 static ref GENERIC_PATTERN: Regex = Regex::new(r"^(.*?)(?:\s+\(.*?\))?$").unwrap();
114
115 static ref DAILY_WEEKLY_PATTERN: Regex = Regex::new(r"^(Daily|Weekly)\s+(.*?)$").unwrap();
117 static ref END_OF_MONTH_PATTERN: Regex = Regex::new(r"^(End of Month)\s+(.*?)$").unwrap();
118 static ref QUARTERLY_PATTERN: Regex = Regex::new(r"^(Quarterly)\s+(.*?)$").unwrap();
119 static ref MONTHLY_PATTERN: Regex = Regex::new(r"^(Monthly)\s+(.*?)$").unwrap();
120 static ref SUFFIX_PATTERN: Regex = Regex::new(r"^(.*?)\s+\(.*?\)$").unwrap();
121 }
122
123 fn clean_asset_name(asset_name: &str) -> String {
125 let normalized_name = normalize_text(asset_name);
127
128 let asset_name = if let Some(captures) = DAILY_WEEKLY_PATTERN.captures(&normalized_name) {
130 captures.get(2).unwrap().as_str().trim()
131 } else if let Some(captures) = END_OF_MONTH_PATTERN.captures(&normalized_name) {
132 captures.get(2).unwrap().as_str().trim()
133 } else if let Some(captures) = QUARTERLY_PATTERN.captures(&normalized_name) {
134 captures.get(2).unwrap().as_str().trim()
135 } else if let Some(captures) = MONTHLY_PATTERN.captures(&normalized_name) {
136 captures.get(2).unwrap().as_str().trim()
137 } else {
138 &normalized_name
139 };
140
141 let asset_name = if let Some(captures) = SUFFIX_PATTERN.captures(asset_name) {
143 captures.get(1).unwrap().as_str().trim()
144 } else {
145 asset_name
146 };
147
148 asset_name.to_string()
149 }
150
151 if let Some(captures) = OPTION_PATTERN.captures(instrument_name) {
152 let asset_name = captures.get(1).unwrap().as_str().trim();
154 ParsedOptionInfo {
155 asset_name: clean_asset_name(asset_name),
156 strike: Some(captures.get(2).unwrap().as_str().to_string()),
157 option_type: Some(captures.get(3).unwrap().as_str().to_string()),
158 }
159 } else if let Some(captures) = SPECIAL_OPTION_PATTERN.captures(instrument_name) {
160 let base_name = captures.get(1).unwrap().as_str().trim();
162 ParsedOptionInfo {
163 asset_name: clean_asset_name(base_name),
164 strike: Some(captures.get(3).unwrap().as_str().to_string()),
165 option_type: Some(captures.get(4).unwrap().as_str().to_string()),
166 }
167 } else if let Some(captures) = INCOMPLETE_PAREN_PATTERN.captures(instrument_name) {
168 let asset_name = captures.get(1).unwrap().as_str().trim();
170 ParsedOptionInfo {
171 asset_name: clean_asset_name(asset_name),
172 strike: Some(captures.get(2).unwrap().as_str().to_string()),
173 option_type: Some(captures.get(3).unwrap().as_str().to_string()),
174 }
175 } else if let Some(captures) = DECIMAL_OPTION_PATTERN.captures(instrument_name) {
176 let asset_name = captures.get(1).unwrap().as_str().trim();
178 ParsedOptionInfo {
179 asset_name: clean_asset_name(asset_name),
180 strike: Some(captures.get(2).unwrap().as_str().to_string()),
181 option_type: Some(captures.get(3).unwrap().as_str().to_string()),
182 }
183 } else if let Some(captures) = GENERIC_PATTERN.captures(instrument_name) {
184 let asset_name = captures.get(1).unwrap().as_str().trim();
186 ParsedOptionInfo {
187 asset_name: clean_asset_name(asset_name),
188 strike: None,
189 option_type: None,
190 }
191 } else {
192 warn!("Could not parse instrument name: {}", instrument_name);
194 ParsedOptionInfo {
195 asset_name: instrument_name.to_string(),
196 strike: None,
197 option_type: None,
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_parse_instrument_name_standard_option() {
208 let info = parse_instrument_name("US Tech 100 19200 CALL ($1)");
209 assert_eq!(info.asset_name, "US Tech 100");
210 assert_eq!(info.strike, Some("19200".to_string()));
211 assert_eq!(info.option_type, Some("CALL".to_string()));
212 }
213
214 #[test]
215 fn test_parse_instrument_name_decimal_strike() {
216 let info = parse_instrument_name("Volatility Index 10.5 PUT ($1)");
217 assert_eq!(info.asset_name, "Volatility Index");
218 assert_eq!(info.strike, Some("10.5".to_string()));
219 assert_eq!(info.option_type, Some("PUT".to_string()));
220 }
221
222 #[test]
223 fn test_parse_instrument_name_no_option() {
224 let info = parse_instrument_name("Germany 40");
225 assert_eq!(info.asset_name, "Germany 40");
226 assert_eq!(info.strike, None);
227 assert_eq!(info.option_type, None);
228 }
229
230 #[test]
231 fn test_parse_instrument_name_with_parenthesis() {
232 let info = parse_instrument_name("US 500 (Mini)");
233 assert_eq!(info.asset_name, "US 500");
234 assert_eq!(info.strike, None);
235 assert_eq!(info.option_type, None);
236 }
237
238 #[test]
239 fn test_parse_instrument_name_special_format() {
240 let info = parse_instrument_name("Weekly Germany 40 (Wed)27500 PUT");
241 assert_eq!(info.asset_name, "Germany 40");
242 assert_eq!(info.strike, Some("27500".to_string()));
243 assert_eq!(info.option_type, Some("PUT".to_string()));
244 }
245
246 #[test]
247 fn test_parse_instrument_name_daily_prefix() {
248 let info = parse_instrument_name("Daily Germany 40 24225 CALL");
249 assert_eq!(info.asset_name, "Germany 40");
250 assert_eq!(info.strike, Some("24225".to_string()));
251 assert_eq!(info.option_type, Some("CALL".to_string()));
252 }
253
254 #[test]
255 fn test_parse_instrument_name_weekly_prefix() {
256 let info = parse_instrument_name("Weekly US Tech 100 19200 CALL");
257 assert_eq!(info.asset_name, "US Tech 100");
258 assert_eq!(info.strike, Some("19200".to_string()));
259 assert_eq!(info.option_type, Some("CALL".to_string()));
260 }
261
262 #[test]
263 fn test_parse_instrument_name_end_of_month_prefix() {
264 let info = parse_instrument_name("End of Month EU Stocks 50 4575 PUT");
265 assert_eq!(info.asset_name, "EU Stocks 50");
266 assert_eq!(info.strike, Some("4575".to_string()));
267 assert_eq!(info.option_type, Some("PUT".to_string()));
268 }
269
270 #[test]
271 fn test_parse_instrument_name_end_of_month_suffix() {
272 let info = parse_instrument_name("US 500 (End of Month) 3200 PUT");
273 assert_eq!(info.asset_name, "US 500");
274 assert_eq!(info.strike, Some("3200".to_string()));
275 assert_eq!(info.option_type, Some("PUT".to_string()));
276 }
277
278 #[test]
279 fn test_parse_instrument_name_quarterly_prefix() {
280 let info = parse_instrument_name("Quarterly GBPUSD 10000 PUT ($1)");
281 assert_eq!(info.asset_name, "GBPUSD");
282 assert_eq!(info.strike, Some("10000".to_string()));
283 assert_eq!(info.option_type, Some("PUT".to_string()));
284 }
285
286 #[test]
287 fn test_parse_instrument_name_weekly_with_day() {
288 let info = parse_instrument_name("Weekly Germany 40 (Mon) 18500 PUT");
289 assert_eq!(info.asset_name, "Germany 40");
290 assert_eq!(info.strike, Some("18500".to_string()));
291 assert_eq!(info.option_type, Some("PUT".to_string()));
292 }
293
294 #[test]
295 fn test_parse_instrument_name_incomplete_parenthesis() {
296 let info = parse_instrument_name("Weekly USDJPY 12950 CALL (Y100");
297 assert_eq!(info.asset_name, "USDJPY");
298 assert_eq!(info.strike, Some("12950".to_string()));
299 assert_eq!(info.option_type, Some("CALL".to_string()));
300 }
301
302 #[test]
303 fn test_parse_instrument_name_with_accents() {
304 let info = parse_instrument_name("Japón 225 18500 CALL");
305 assert_eq!(info.asset_name, "Japan 225");
306 assert_eq!(info.strike, Some("18500".to_string()));
307 assert_eq!(info.option_type, Some("CALL".to_string()));
308 }
309}