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