1use crate::error::AppError;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
9pub struct InstrumentInfo {
10 pub underlying: Option<String>,
12 pub strike: Option<f64>,
14 pub option_type: Option<String>,
16 pub is_option: bool,
18}
19
20pub fn parse_instrument_name(instrument_name: &str) -> Result<InstrumentInfo, AppError> {
22 let mut info = InstrumentInfo::default();
23
24 if instrument_name.contains("Cargo por tarifa")
26 || instrument_name.contains("Daily Admin Fee")
27 || instrument_name.contains("Fee")
28 || (instrument_name.starts_with("End ") && !instrument_name.starts_with("End of Month"))
29 || instrument_name.contains("Funds")
30 || instrument_name.contains("Funds Transfer")
31 {
32 return Ok(info); }
34
35 let option_type_re = Regex::new(r"(?i)(CALL|PUT|Call|Put)\b").unwrap();
37 if let Some(cap) = option_type_re.captures(instrument_name) {
38 let opt_type = cap[1].to_uppercase();
39 info.option_type = Some(opt_type);
40 info.is_option = true;
41 } else {
42 return Ok(info);
44 }
45
46 let strike_re = Regex::new(r"\b(\d+(?:\.\d+)?)\s*(?:CALL|PUT|Call|Put)|(?:CALL|PUT|Call|Put)\s*(?:a\s*)?(\d+(?:\.\d+)?)").unwrap();
48 if let Some(cap) = strike_re.captures(instrument_name) {
49 let strike_str = cap.get(1).or_else(|| cap.get(2)).unwrap().as_str();
51 if let Ok(strike_val) = strike_str.parse::<f64>() {
52 info.strike = Some(strike_val);
53 }
54 }
55
56 extract_underlying(&mut info, instrument_name);
58
59 Ok(info)
60}
61
62fn extract_underlying(info: &mut InstrumentInfo, instrument_name: &str) {
64 let known_patterns: HashMap<&str, &str> = [
66 ("Eu Stocks 50", "EU50"),
68 ("EU Stocks 50", "EU50"),
69 ("EU50", "EU50"),
70 ("Germany 40", "GER40"),
72 ("GER40", "GER40"),
73 ("US 500", "US500"),
75 ("US500", "US500"),
76 ("US Tech 100", "USTECH"),
78 ("USTECH", "USTECH"),
79 ("Gold Futures", "GOLD"),
81 ("Gold (", "GOLD"),
82 ("GOLD", "GOLD"),
83 ("Gold", "GOLD"),
84 ("Silver Futures", "SILVER"),
86 ("Silver (", "SILVER"),
87 ("SILVER", "SILVER"),
88 ("Silver", "SILVER"),
89 ("Natural Gas", "NATGAS"),
91 ("NATGAS", "NATGAS"),
92 ("FTSE", "UK100"),
94 ("UK100", "UK100"),
95 ("Wall Street", "US30"),
97 ("US30", "US30"),
98 ("Crude", "OIL"),
100 ("Oil", "OIL"),
101 ("OIL", "OIL"),
102 ("France 40", "FRA40"),
104 ("FRA40", "FRA40"),
105 ("Bitcoin", "BITCOIN"),
107 ("BITCOIN", "BITCOIN"),
108 ("Ether", "ETHEREUM"),
110 ("ETHEREUM", "ETHEREUM"),
111 ("Paypal", "PAYPAL"),
113 ("PAYPAL", "PAYPAL"),
114 ]
115 .iter()
116 .cloned()
117 .collect();
118
119 if instrument_name.starts_with("End of Month") {
121 let parts: Vec<&str> = instrument_name.split_whitespace().collect();
122 if parts.len() >= 5 && parts[3] == "Germany" && parts[4] == "40" {
123 info.underlying = Some("GER40".to_string());
124 return;
125 } else if parts.len() >= 6 && parts[3] == "EU" && parts[4] == "Stocks" && parts[5] == "50" {
126 info.underlying = Some("EU50".to_string());
127 return;
128 }
129 }
130
131 if instrument_name.starts_with("Option premium") {
133 let option_premium_re = Regex::new(r"Option premium (?:received|paid) (.*?)(?:\s+\d+(?:\.\d+)?|\s+\(Wed\)\d+|\s+\(End of Month\)\d+|\s+\(Mon\)\d+|\s+\(£\d+\)|\s+\(E\d+\)|\s+\(\$\d+\))\s*(?:CALL|PUT)").unwrap();
135 if let Some(cap) = option_premium_re.captures(instrument_name) {
136 let underlying_text = cap.get(1).unwrap().as_str().trim();
137
138 for (pattern, standard_name) in &known_patterns {
140 if underlying_text.contains(pattern) {
141 info.underlying = Some(standard_name.to_string());
142 return;
143 }
144 }
145
146 info.underlying = Some(underlying_text.to_string());
148 return;
149 }
150 }
151
152 if instrument_name.contains("Barrier Call") || instrument_name.contains("Barrier Put") {
154 if instrument_name.starts_with("Bitcoin") {
155 info.underlying = Some("BITCOIN".to_string());
156 return;
157 } else if instrument_name.starts_with("Ether") {
158 info.underlying = Some("ETHEREUM".to_string());
159 return;
160 }
161 }
162
163 for (pattern, standard_name) in &known_patterns {
165 if instrument_name.contains(pattern) {
166 info.underlying = Some(standard_name.to_string());
167 return;
168 }
169 }
170
171 let parts: Vec<&str> = instrument_name.split_whitespace().collect();
173 if !parts.is_empty() {
174 if parts[0] == "Daily" || parts[0] == "Weekly" {
176 if parts.len() > 1 {
177 info.underlying = Some(parts[1].to_string());
178 }
179 } else {
180 info.underlying = Some(parts[0].to_string());
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use serde_json::Value;
190
191 pub fn parse_instrument_from_json(json_str: &str) -> Result<InstrumentInfo, AppError> {
193 let json_value: Value = match serde_json::from_str(json_str) {
195 Ok(value) => value,
196 Err(e) => return Err(AppError::SerializationError(e.to_string())),
197 };
198
199 let instrument_name = match json_value.get("instrumentName") {
201 Some(Value::String(name)) => name,
202 _ => {
203 return Err(AppError::SerializationError(
204 "Missing or invalid instrumentName field".to_string(),
205 ));
206 }
207 };
208
209 parse_instrument_name(instrument_name)
210 }
211
212 #[test]
213 fn test_barrier_option() {
214 let json = r#"{"instrumentName":"Bitcoin Barrier Call a 69650 COMM DIAAAANMBDWXTAS Tipo de cambio 0.9330"}"#;
215 let info = parse_instrument_from_json(json).unwrap();
216 assert_eq!(info.underlying, Some("BITCOIN".to_string()));
217 assert_eq!(info.strike, Some(69650.0));
218 assert_eq!(info.option_type, Some("CALL".to_string()));
219 assert!(info.is_option);
220 }
221
222 #[test]
223 fn test_ether_barrier() {
224 let json = r#"{"instrumentName":"Ether Barrier Call a 3852 COMM DIAAAANSKFGLDAQ Tipo de cambio 0.9587"}"#;
225 let info = parse_instrument_from_json(json).unwrap();
226 assert_eq!(info.underlying, Some("ETHEREUM".to_string()));
227 assert_eq!(info.strike, Some(3852.0));
228 assert_eq!(info.option_type, Some("CALL".to_string()));
229 assert!(info.is_option);
230 }
231
232 #[test]
233 fn test_regular_option() {
234 let json =
235 r#"{"instrumentName":"Daily Eu Stocks 50 5184 CALL (EUR1) COMM DIAAAAN2FL4MTA6"}"#;
236 let info = parse_instrument_from_json(json).unwrap();
237 assert_eq!(info.underlying, Some("EU50".to_string()));
238 assert_eq!(info.strike, Some(5184.0));
239 assert_eq!(info.option_type, Some("CALL".to_string()));
240 assert!(info.is_option);
241 }
242
243 #[test]
244 fn test_fee_entry() {
245 let json = r#"{"instrumentName":"Fee charge for charts on April 25"}"#;
246 let info = parse_instrument_from_json(json).unwrap();
247 assert!(!info.is_option);
248 assert_eq!(info.underlying, None);
249 }
250
251 #[test]
252 fn test_financing_adjustment() {
253 let json = r#"{"instrumentName":"Daily Financing Adjustment - Bitcoin Barrier Call for 1 day USD converted at 0.9285"}"#;
254 let info = parse_instrument_from_json(json).unwrap();
255 assert!(info.is_option);
256 }
257
258 #[test]
259 fn test_weekly_option() {
260 let json = r#"{"instrumentName":"Weekly Germany 40 (Wed)19900 PUT COMM DIAAAAPDSPMDQAY"}"#;
261 let info = parse_instrument_from_json(json).unwrap();
262 assert_eq!(info.underlying, Some("GER40".to_string()));
263 assert_eq!(info.strike, Some(19900.0));
264 assert_eq!(info.option_type, Some("PUT".to_string()));
265 assert!(info.is_option);
266 }
267
268 #[test]
269 fn test_option_premium() {
270 let json = r#"{"instrumentName":"Option premium received US 500 6040 CALL ($1) Tipo de cambio 0.8947665"}"#;
271 let info = parse_instrument_from_json(json).unwrap();
272 assert_eq!(info.underlying, Some("US500".to_string()));
273 assert_eq!(info.strike, Some(6040.0));
274 assert_eq!(info.option_type, Some("CALL".to_string()));
275 assert!(info.is_option);
276 }
277
278 #[test]
279 fn test_option_premium_germany() {
280 let json =
281 r#"{"instrumentName":"Option premium received Weekly Germany 40 (Wed)23450 PUT"}"#;
282 let info = parse_instrument_from_json(json).unwrap();
283 assert_eq!(info.underlying, Some("GER40".to_string()));
284 assert_eq!(info.strike, Some(23450.0));
285 assert_eq!(info.option_type, Some("PUT".to_string()));
286 assert!(info.is_option);
287 }
288
289 #[test]
290 fn test_end_of_month_option() {
291 let json = r#"{"instrumentName":"Option premium paid End of Month Germany 40 23500 CALL"}"#;
292 let info = parse_instrument_from_json(json).unwrap();
293 assert_eq!(info.underlying, Some("GER40".to_string()));
294 assert_eq!(info.strike, Some(23500.0));
295 assert_eq!(info.option_type, Some("CALL".to_string()));
296 assert!(info.is_option);
297 }
298
299 #[test]
300 fn test_weekly_us_tech_option() {
301 let json =
302 r#"{"instrumentName":"Option premium received Weekly US Tech 100 (Mon) 21550 PUT"}"#;
303 let info = parse_instrument_from_json(json).unwrap();
304 assert_eq!(info.underlying, Some("USTECH".to_string()));
305 assert_eq!(info.strike, Some(21550.0));
306 assert_eq!(info.option_type, Some("PUT".to_string()));
307 assert!(info.is_option);
308 }
309
310 #[test]
311 fn test_oil_weekly_option() {
312 let json = r#"{"instrumentName":"Option premium received Oil Weekly (Dec Fut) 7150 CALL"}"#;
313 let info = parse_instrument_from_json(json).unwrap();
314 assert_eq!(info.underlying, Some("OIL".to_string()));
315 assert_eq!(info.strike, Some(7150.0));
316 assert_eq!(info.option_type, Some("CALL".to_string()));
317 assert!(info.is_option);
318 }
319
320 #[test]
321 fn test_gold_future_option() {
322 let json =
323 r#"{"instrumentName":"Option premium paid Weekly Gold (Feb Future) 2650 PUT ($1)"}"#;
324 let info = parse_instrument_from_json(json).unwrap();
325 assert_eq!(info.underlying, Some("GOLD".to_string()));
326 assert_eq!(info.strike, Some(2650.0));
327 assert_eq!(info.option_type, Some("PUT".to_string()));
328 assert!(info.is_option);
329 }
330
331 #[test]
332 fn test_ge40_call_option() {
333 let json =
334 r#"{"instrumentName":"End of Month Germany 40 22800 CALL COMM DIAAAAPBC65ZQAP"}"#;
335 let info = parse_instrument_from_json(json).unwrap();
336 assert_eq!(info.underlying, Some("GER40".to_string()));
337 assert_eq!(info.strike, Some(22800.0));
338 assert_eq!(info.option_type, Some("CALL".to_string()));
339 assert!(info.is_option);
340 }
341
342 #[test]
343 fn test_ge40_put_option() {
344 let json = r#"{"instrumentName":"End of Month Germany 40 22000 PUT COMM DIAAAAPAWDLTNAB"}"#;
345 let info = parse_instrument_from_json(json).unwrap();
346 assert_eq!(info.underlying, Some("GER40".to_string()));
347 assert_eq!(info.strike, Some(22000.0));
348 assert_eq!(info.option_type, Some("PUT".to_string()));
349 assert!(info.is_option);
350 }
351
352 #[test]
353 fn test_ftse_option() {
354 let json = r#"{"instrumentName":"Option premium paid FTSE 8250 CALL (£1) Tipo de cambio 1.17471"}"#;
355 let info = parse_instrument_from_json(json).unwrap();
356 assert_eq!(info.underlying, Some("UK100".to_string()));
357 assert_eq!(info.strike, Some(8250.0));
358 assert_eq!(info.option_type, Some("CALL".to_string()));
359 assert!(info.is_option);
360 }
361
362 #[test]
363 fn test_funds_transfer() {
364 let json = r#"{"instrumentName":"Funds Transfer from Barreras y Opciones"}"#;
365 let info = parse_instrument_from_json(json).unwrap();
366 assert!(!info.is_option);
367 assert_eq!(info.underlying, None);
368 }
369}