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