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                && let Some((mon_str, year_str)) = rest.split_once('-')
41            {
42                // Try to parse the day
43                if let Ok(day) = day_str.parse::<u32>() {
44                    let month = chrono::Month::from_str(mon_str).ok()?;
45                    let year = 2000 + year_str.parse::<i32>().ok()?;
46
47                    // Return the exact date
48                    return NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
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            .replace(',', "") // Remove comma separators for thousands
106            .parse::<f64>()
107            .unwrap_or(0.0);
108
109        let expiry = parse_period(&raw.period);
110
111        let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < 1.0;
112
113        StoreTransaction {
114            deal_date,
115            underlying,
116            strike,
117            option_type,
118            expiry,
119            transaction_type: raw.transaction_type.clone(),
120            pnl_eur,
121            reference: raw.reference.clone(),
122            is_fee,
123            raw_json: raw.to_string(),
124        }
125    }
126}
127
128impl From<&AccountTransaction> for StoreTransaction {
129    fn from(raw: &AccountTransaction) -> Self {
130        StoreTransaction::from(raw.clone())
131    }
132}
133
134/// Collection of processed transactions from IG Markets
135///
136/// This struct is a wrapper around a vector of `StoreTransaction` objects
137/// and provides convenient methods for accessing and converting transaction data.
138pub struct TransactionList(pub Vec<StoreTransaction>);
139
140impl AsRef<[StoreTransaction]> for TransactionList {
141    fn as_ref(&self) -> &[StoreTransaction] {
142        &self.0[..]
143    }
144}
145
146impl From<&Vec<AccountTransaction>> for TransactionList {
147    fn from(raw: &Vec<AccountTransaction>) -> Self {
148        TransactionList(
149            raw.iter() // Use iter() instead of into_iter() for references
150                .map(StoreTransaction::from) // This assumes there is an impl From<&AccountTransaction> for StoreTransaction
151                .collect(),
152        )
153    }
154}