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
35impl ParsedMarketData {
36 #[must_use]
48 #[inline]
49 pub fn is_call(&self) -> bool {
50 self.option_type.as_deref() == Some("CALL")
51 }
52
53 #[must_use]
63 #[inline]
64 pub fn is_put(&self) -> bool {
65 self.option_type.as_deref() == Some("PUT")
66 }
67}
68
69pub fn normalize_text(text: &str) -> String {
74 if text.contains("Japón") {
76 return text.replace("Japón", "Japan");
77 }
78
79 let mut result = String::with_capacity(text.len());
80 for c in text.chars() {
81 match c {
82 'á' | 'à' | 'ä' | 'â' | 'ã' => result.push('a'),
83 'é' | 'è' | 'ë' | 'ê' => result.push('e'),
84 'í' | 'ì' | 'ï' | 'î' => result.push('i'),
85 'ó' | 'ò' | 'ö' | 'ô' | 'õ' => result.push('o'),
86 'ú' | 'ù' | 'ü' | 'û' => result.push('u'),
87 'ñ' => result.push('n'),
88 'ç' => result.push('c'),
89 'Á' | 'À' | 'Ä' | 'Â' | 'Ã' => result.push('A'),
90 'É' | 'È' | 'Ë' | 'Ê' => result.push('E'),
91 'Í' | 'Ì' | 'Ï' | 'Î' => result.push('I'),
92 'Ó' | 'Ò' | 'Ö' | 'Ô' | 'Õ' => result.push('O'),
93 'Ú' | 'Ù' | 'Ü' | 'Û' => result.push('U'),
94 'Ñ' => result.push('N'),
95 'Ç' => result.push('C'),
96 _ => result.push(c),
97 }
98 }
99 result
100}
101
102pub fn parse_instrument_name(instrument_name: &str) -> ParsedOptionInfo {
120 lazy_static::lazy_static! {
123 static ref OPTION_PATTERN: Regex = Regex::new(r"^(.*?)\s+(\d+(?:\.\d+)?)\s+((?i:CALL|PUT))(?:\s+\(.*?\))?$").expect("valid option regex");
127
128 static ref SPECIAL_OPTION_PATTERN: Regex = Regex::new(r"^(.*?)\s+\(([^)]+)\)(\d+)\s+((?i:CALL|PUT))(?:\s+\(.*?\))?$").expect("valid special option regex");
130
131 static ref INCOMPLETE_PAREN_PATTERN: Regex = Regex::new(r"^(.*?)\s+(\d+(?:\.\d+)?)\s+((?i:CALL|PUT))\s+\([^)]*$").expect("valid incomplete paren regex");
133
134 static ref GENERIC_PATTERN: Regex = Regex::new(r"^(.*?)(?:\s+\(.*?\))?$").expect("valid generic regex");
136
137 static ref DAILY_WEEKLY_PATTERN: Regex = Regex::new(r"^(Daily|Weekly)\s+(.*?)$").expect("valid daily/weekly regex");
139 static ref END_OF_MONTH_PATTERN: Regex = Regex::new(r"^(End of Month)\s+(.*?)$").expect("valid end-of-month regex");
140 static ref QUARTERLY_PATTERN: Regex = Regex::new(r"^(Quarterly)\s+(.*?)$").expect("valid quarterly regex");
141 static ref MONTHLY_PATTERN: Regex = Regex::new(r"^(Monthly)\s+(.*?)$").expect("valid monthly regex");
142 static ref SUFFIX_PATTERN: Regex = Regex::new(r"^(.*?)\s+\(.*?\)$").expect("valid suffix regex");
143 }
144
145 fn clean_asset_name(asset_name: &str) -> String {
147 let normalized_name = normalize_text(asset_name);
149
150 let asset_name = if let Some(captures) = DAILY_WEEKLY_PATTERN.captures(&normalized_name) {
152 captures.get(2).map_or("", |m| m.as_str()).trim()
153 } else if let Some(captures) = END_OF_MONTH_PATTERN.captures(&normalized_name) {
154 captures.get(2).map_or("", |m| m.as_str()).trim()
155 } else if let Some(captures) = QUARTERLY_PATTERN.captures(&normalized_name) {
156 captures.get(2).map_or("", |m| m.as_str()).trim()
157 } else if let Some(captures) = MONTHLY_PATTERN.captures(&normalized_name) {
158 captures.get(2).map_or("", |m| m.as_str()).trim()
159 } else {
160 &normalized_name
161 };
162
163 let asset_name = if let Some(captures) = SUFFIX_PATTERN.captures(asset_name) {
165 captures.get(1).map_or("", |m| m.as_str()).trim()
166 } else {
167 asset_name
168 };
169
170 asset_name.to_string()
171 }
172
173 if let Some(captures) = OPTION_PATTERN.captures(instrument_name) {
174 let asset_name = captures.get(1).map_or("", |m| m.as_str()).trim();
176 ParsedOptionInfo {
177 asset_name: clean_asset_name(asset_name),
178 strike: captures.get(2).map(|m| m.as_str().to_string()),
179 option_type: captures.get(3).map(|m| m.as_str().to_uppercase()),
180 }
181 } else if let Some(captures) = SPECIAL_OPTION_PATTERN.captures(instrument_name) {
182 let base_name = captures.get(1).map_or("", |m| m.as_str()).trim();
184 ParsedOptionInfo {
185 asset_name: clean_asset_name(base_name),
186 strike: captures.get(3).map(|m| m.as_str().to_string()),
187 option_type: captures.get(4).map(|m| m.as_str().to_uppercase()),
188 }
189 } else if let Some(captures) = INCOMPLETE_PAREN_PATTERN.captures(instrument_name) {
190 let asset_name = captures.get(1).map_or("", |m| m.as_str()).trim();
192 ParsedOptionInfo {
193 asset_name: clean_asset_name(asset_name),
194 strike: captures.get(2).map(|m| m.as_str().to_string()),
195 option_type: captures.get(3).map(|m| m.as_str().to_uppercase()),
196 }
197 } else if let Some(captures) = GENERIC_PATTERN.captures(instrument_name) {
198 let asset_name = captures.get(1).map_or("", |m| m.as_str()).trim();
200 ParsedOptionInfo {
201 asset_name: clean_asset_name(asset_name),
202 strike: None,
203 option_type: None,
204 }
205 } else {
206 warn!("Could not parse instrument name: {}", instrument_name);
208 ParsedOptionInfo {
209 asset_name: instrument_name.to_string(),
210 strike: None,
211 option_type: None,
212 }
213 }
214}
215
216pub fn deserialize_null_as_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
218where
219 D: serde::Deserializer<'de>,
220 T: serde::Deserialize<'de>,
221{
222 let opt = Option::deserialize(deserializer)?;
223 Ok(opt.unwrap_or_default())
224}
225
226pub fn deserialize_nullable_status<'de, D>(deserializer: D) -> Result<Status, D::Error>
229where
230 D: Deserializer<'de>,
231{
232 let opt = Option::deserialize(deserializer)?;
233 Ok(opt.unwrap_or(Status::Open))
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_parse_instrument_name_standard_option() {
242 let info = parse_instrument_name("US Tech 100 19200 CALL ($1)");
243 assert_eq!(info.asset_name, "US Tech 100");
244 assert_eq!(info.strike, Some("19200".to_string()));
245 assert_eq!(info.option_type, Some("CALL".to_string()));
246 }
247
248 #[test]
249 fn test_parse_instrument_name_mixed_case_option_type() {
250 let info = parse_instrument_name("Wall Street 50000 Call ($1)");
254 assert_eq!(info.asset_name, "Wall Street");
255 assert_eq!(info.strike, Some("50000".to_string()));
256 assert_eq!(info.option_type, Some("CALL".to_string()));
257
258 let info = parse_instrument_name("Wall Street 50000 Put ($1)");
259 assert_eq!(info.option_type, Some("PUT".to_string()));
260 }
261
262 #[test]
263 fn test_parse_instrument_name_decimal_strike() {
264 let info = parse_instrument_name("Volatility Index 10.5 PUT ($1)");
267 assert_eq!(info.asset_name, "Volatility Index");
268 assert_eq!(info.strike, Some("10.5".to_string()));
269 assert_eq!(info.option_type, Some("PUT".to_string()));
270 }
271
272 #[test]
273 fn test_parse_instrument_name_decimal_strike_no_suffix() {
274 let info = parse_instrument_name("Volatility Index 10.5 CALL");
277 assert_eq!(info.asset_name, "Volatility Index");
278 assert_eq!(info.strike, Some("10.5".to_string()));
279 assert_eq!(info.option_type, Some("CALL".to_string()));
280 }
281
282 #[test]
283 fn test_parse_instrument_name_no_option() {
284 let info = parse_instrument_name("Germany 40");
285 assert_eq!(info.asset_name, "Germany 40");
286 assert_eq!(info.strike, None);
287 assert_eq!(info.option_type, None);
288 }
289
290 fn market_with_option_type(option_type: Option<&str>) -> ParsedMarketData {
291 ParsedMarketData {
292 epic: "OP.D.OTCSPX3.6910C.IP".to_string(),
293 instrument_name: "US 500 6910 CALL ($1)".to_string(),
294 expiry: "DEC-25".to_string(),
295 asset_name: "US 500".to_string(),
296 strike: Some("6910".to_string()),
297 option_type: option_type.map(str::to_string),
298 }
299 }
300
301 #[test]
302 fn test_parsed_market_data_is_call_uses_parsed_option_type() {
303 let call = market_with_option_type(Some("CALL"));
304 assert!(call.is_call());
305 assert!(!call.is_put());
306 }
307
308 #[test]
309 fn test_parsed_market_data_is_put_uses_parsed_option_type() {
310 let put = market_with_option_type(Some("PUT"));
311 assert!(put.is_put());
312 assert!(!put.is_call());
313 }
314
315 #[test]
316 fn test_parsed_market_data_no_option_type_is_neither() {
317 let none = market_with_option_type(None);
320 assert!(!none.is_call());
321 assert!(!none.is_put());
322 }
323
324 #[test]
325 fn test_parse_instrument_name_with_parenthesis() {
326 let info = parse_instrument_name("US 500 (Mini)");
327 assert_eq!(info.asset_name, "US 500");
328 assert_eq!(info.strike, None);
329 assert_eq!(info.option_type, None);
330 }
331
332 #[test]
333 fn test_parse_instrument_name_special_format() {
334 let info = parse_instrument_name("Weekly Germany 40 (Wed)27500 PUT");
335 assert_eq!(info.asset_name, "Germany 40");
336 assert_eq!(info.strike, Some("27500".to_string()));
337 assert_eq!(info.option_type, Some("PUT".to_string()));
338 }
339
340 #[test]
341 fn test_parse_instrument_name_daily_prefix() {
342 let info = parse_instrument_name("Daily Germany 40 24225 CALL");
343 assert_eq!(info.asset_name, "Germany 40");
344 assert_eq!(info.strike, Some("24225".to_string()));
345 assert_eq!(info.option_type, Some("CALL".to_string()));
346 }
347
348 #[test]
349 fn test_parse_instrument_name_weekly_prefix() {
350 let info = parse_instrument_name("Weekly US Tech 100 19200 CALL");
351 assert_eq!(info.asset_name, "US Tech 100");
352 assert_eq!(info.strike, Some("19200".to_string()));
353 assert_eq!(info.option_type, Some("CALL".to_string()));
354 }
355
356 #[test]
357 fn test_parse_instrument_name_end_of_month_prefix() {
358 let info = parse_instrument_name("End of Month EU Stocks 50 4575 PUT");
359 assert_eq!(info.asset_name, "EU Stocks 50");
360 assert_eq!(info.strike, Some("4575".to_string()));
361 assert_eq!(info.option_type, Some("PUT".to_string()));
362 }
363
364 #[test]
365 fn test_parse_instrument_name_end_of_month_suffix() {
366 let info = parse_instrument_name("US 500 (End of Month) 3200 PUT");
367 assert_eq!(info.asset_name, "US 500");
368 assert_eq!(info.strike, Some("3200".to_string()));
369 assert_eq!(info.option_type, Some("PUT".to_string()));
370 }
371
372 #[test]
373 fn test_parse_instrument_name_quarterly_prefix() {
374 let info = parse_instrument_name("Quarterly GBPUSD 10000 PUT ($1)");
375 assert_eq!(info.asset_name, "GBPUSD");
376 assert_eq!(info.strike, Some("10000".to_string()));
377 assert_eq!(info.option_type, Some("PUT".to_string()));
378 }
379
380 #[test]
381 fn test_parse_instrument_name_weekly_with_day() {
382 let info = parse_instrument_name("Weekly Germany 40 (Mon) 18500 PUT");
383 assert_eq!(info.asset_name, "Germany 40");
384 assert_eq!(info.strike, Some("18500".to_string()));
385 assert_eq!(info.option_type, Some("PUT".to_string()));
386 }
387
388 #[test]
389 fn test_parse_instrument_name_incomplete_parenthesis() {
390 let info = parse_instrument_name("Weekly USDJPY 12950 CALL (Y100");
391 assert_eq!(info.asset_name, "USDJPY");
392 assert_eq!(info.strike, Some("12950".to_string()));
393 assert_eq!(info.option_type, Some("CALL".to_string()));
394 }
395
396 #[test]
397 fn test_parse_instrument_name_with_accents() {
398 let info = parse_instrument_name("Japón 225 18500 CALL");
399 assert_eq!(info.asset_name, "Japan 225");
400 assert_eq!(info.strike, Some("18500".to_string()));
401 assert_eq!(info.option_type, Some("CALL".to_string()));
402 }
403}