ig_client/application/models/
transaction.rs

1use crate::application::models::account::AccountTransaction;
2use crate::impl_json_display;
3use crate::utils::parsing::{InstrumentInfo, 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: InstrumentInfo = parse_instrument_name(&raw.instrument_name).unwrap();
91        let underlying = instrument_info.underlying;
92        let strike = instrument_info.strike;
93        let option_type = instrument_info.option_type;
94        let deal_date = NaiveDateTime::parse_from_str(&raw.date_utc, "%Y-%m-%dT%H:%M:%S")
95            .map(|naive| naive.and_utc())
96            .unwrap_or_else(|_| Utc::now());
97        let pnl_eur = raw
98            .profit_and_loss
99            .trim_start_matches('E')
100            .parse::<f64>()
101            .unwrap_or(0.0);
102
103        let expiry = parse_period(&raw.period);
104
105        let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < 1.0;
106
107        StoreTransaction {
108            deal_date,
109            underlying,
110            strike,
111            option_type,
112            expiry,
113            transaction_type: raw.transaction_type.clone(),
114            pnl_eur,
115            reference: raw.reference.clone(),
116            is_fee,
117            raw_json: raw.to_string(),
118        }
119    }
120}
121
122impl From<&AccountTransaction> for StoreTransaction {
123    fn from(raw: &AccountTransaction) -> Self {
124        StoreTransaction::from(raw.clone())
125    }
126}
127
128/// Collection of processed transactions from IG Markets
129///
130/// This struct is a wrapper around a vector of `StoreTransaction` objects
131/// and provides convenient methods for accessing and converting transaction data.
132pub struct TransactionList(pub Vec<StoreTransaction>);
133
134impl AsRef<[StoreTransaction]> for TransactionList {
135    fn as_ref(&self) -> &[StoreTransaction] {
136        &self.0[..]
137    }
138}
139
140impl From<&Vec<AccountTransaction>> for TransactionList {
141    fn from(raw: &Vec<AccountTransaction>) -> Self {
142        TransactionList(
143            raw.iter() // Usa iter() en lugar de into_iter() para referencias
144                .map(StoreTransaction::from) // Esto asume que hay un impl From<&AccountTransaction> for StoreTransaction
145                .collect(),
146        )
147    }
148}