ig_client/application/models/
transaction.rs

1use crate::application::models::account::AccountTransaction;
2use crate::impl_json_display;
3use crate::utils::parsing::{ParsedOptionInfo, parse_instrument_name};
4use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Utc, Weekday};
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// Represents a processed transaction from IG Markets with parsed fields
9#[derive(Debug, Serialize, Deserialize)]
10pub struct StoreTransaction {
11    /// Date and time when the transaction was executed
12    pub deal_date: DateTime<Utc>,
13    /// Underlying asset or instrument (e.g., "GOLD", "US500")
14    pub underlying: Option<String>,
15    /// Strike price for options
16    pub strike: Option<f64>,
17    /// Type of option ("CALL" or "PUT")
18    pub option_type: Option<String>,
19    /// Expiration date for options
20    pub expiry: Option<NaiveDate>,
21    /// Type of transaction (e.g., "DEAL", "WITH")
22    pub transaction_type: String,
23    /// Profit and loss in EUR
24    pub pnl_eur: f64,
25    /// Unique reference for the transaction
26    pub reference: String,
27    /// Whether this transaction is a fee
28    pub is_fee: bool,
29    /// Original JSON string of the transaction
30    pub raw_json: String,
31}
32
33impl_json_display!(StoreTransaction);
34
35impl From<AccountTransaction> for StoreTransaction {
36    fn from(raw: AccountTransaction) -> Self {
37        fn parse_period(period: &str) -> Option<NaiveDate> {
38            // For format "DD-MON-YY"
39            if let Some((day_str, rest)) = period.split_once('-') {
40                if let Some((mon_str, year_str)) = rest.split_once('-') {
41                    // Try to parse the day
42                    if let Ok(day) = day_str.parse::<u32>() {
43                        let month = chrono::Month::from_str(mon_str).ok()?;
44                        let year = 2000 + year_str.parse::<i32>().ok()?;
45
46                        // Return the exact date
47                        return NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
48                    }
49                }
50            }
51
52            // For format "MON-YY"
53            if let Some((mon_str, year_str)) = period.split_once('-') {
54                let month = chrono::Month::from_str(mon_str).ok()?;
55                let year = 2000 + year_str.parse::<i32>().ok()?;
56
57                // Get the first day of the month
58                let first_of_month = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1)?;
59
60                // Get the first day of the previous month
61                let prev_month = if month.number_from_month() == 1 {
62                    // If January, go to December of previous year
63                    NaiveDate::from_ymd_opt(year - 1, 12, 1)?
64                } else {
65                    // Otherwise, just go to previous month
66                    NaiveDate::from_ymd_opt(year, month.number_from_month() - 1, 1)?
67                };
68
69                // Find the last day of the previous month
70                let last_day_of_prev_month = if prev_month.month() == 12 {
71                    // December has 31 days
72                    NaiveDate::from_ymd_opt(prev_month.year(), 12, 31)?
73                } else {
74                    // For other months, the last day is one day before the first of current month
75                    first_of_month - Duration::days(1)
76                };
77
78                // Calculate how many days to go back to find the last Wednesday
79                let days_back = (last_day_of_prev_month.weekday().num_days_from_monday() + 7
80                    - Weekday::Wed.num_days_from_monday())
81                    % 7;
82
83                // Get the last Wednesday
84                return Some(last_day_of_prev_month - Duration::days(days_back as i64));
85            }
86
87            None
88        }
89
90        let instrument_info: ParsedOptionInfo = parse_instrument_name(&raw.instrument_name);
91        let underlying = Some(instrument_info.asset_name);
92        let strike = match instrument_info {
93            ParsedOptionInfo {
94                strike: Some(s), ..
95            } => Some(s.parse::<f64>().ok()).flatten(),
96            _ => None,
97        };
98        let option_type = instrument_info.option_type;
99        let deal_date = NaiveDateTime::parse_from_str(&raw.date_utc, "%Y-%m-%dT%H:%M:%S")
100            .map(|naive| naive.and_utc())
101            .unwrap_or_else(|_| Utc::now());
102        let pnl_eur = raw
103            .profit_and_loss
104            .trim_start_matches('E')
105            .parse::<f64>()
106            .unwrap_or(0.0);
107
108        let expiry = parse_period(&raw.period);
109
110        let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < 1.0;
111
112        StoreTransaction {
113            deal_date,
114            underlying,
115            strike,
116            option_type,
117            expiry,
118            transaction_type: raw.transaction_type.clone(),
119            pnl_eur,
120            reference: raw.reference.clone(),
121            is_fee,
122            raw_json: raw.to_string(),
123        }
124    }
125}
126
127impl From<&AccountTransaction> for StoreTransaction {
128    fn from(raw: &AccountTransaction) -> Self {
129        StoreTransaction::from(raw.clone())
130    }
131}
132
133/// Collection of processed transactions from IG Markets
134///
135/// This struct is a wrapper around a vector of `StoreTransaction` objects
136/// and provides convenient methods for accessing and converting transaction data.
137pub struct TransactionList(pub Vec<StoreTransaction>);
138
139impl AsRef<[StoreTransaction]> for TransactionList {
140    fn as_ref(&self) -> &[StoreTransaction] {
141        &self.0[..]
142    }
143}
144
145impl From<&Vec<AccountTransaction>> for TransactionList {
146    fn from(raw: &Vec<AccountTransaction>) -> Self {
147        TransactionList(
148            raw.iter() // Usa iter() en lugar de into_iter() para referencias
149                .map(StoreTransaction::from) // Esto asume que hay un impl From<&AccountTransaction> for StoreTransaction
150                .collect(),
151        )
152    }
153}