ig_client/presentation/
transaction.rs

1use crate::presentation::account::AccountTransaction;
2use crate::utils::parsing::{ParsedOptionInfo, parse_instrument_name};
3use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, Utc, Weekday};
4use pretty_simple_display::DisplaySimple;
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// Represents a processed transaction from IG Markets with parsed fields
9#[derive(Debug, DisplaySimple, Serialize, Deserialize, PartialEq, Clone, Default)]
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 From<AccountTransaction> for StoreTransaction {
34    fn from(raw: AccountTransaction) -> Self {
35        fn parse_period(period: &str) -> Option<NaiveDate> {
36            // For format "DD-MON-YY"
37            if let Some((day_str, rest)) = period.split_once('-')
38                && let Some((mon_str, year_str)) = rest.split_once('-')
39            {
40                // Try to parse the day
41                if let Ok(day) = day_str.parse::<u32>() {
42                    let month = chrono::Month::from_str(mon_str).ok()?;
43                    let year = 2000 + year_str.parse::<i32>().ok()?;
44
45                    // Return the exact date
46                    return NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
47                }
48            }
49
50            // For format "MON-YY"
51            if let Some((mon_str, year_str)) = period.split_once('-') {
52                let month = chrono::Month::from_str(mon_str).ok()?;
53                let year = 2000 + year_str.parse::<i32>().ok()?;
54
55                // Get the first day of the month
56                let first_of_month = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1)?;
57
58                // Get the first day of the previous month
59                let prev_month = if month.number_from_month() == 1 {
60                    // If January, go to December of previous year
61                    NaiveDate::from_ymd_opt(year - 1, 12, 1)?
62                } else {
63                    // Otherwise, just go to previous month
64                    NaiveDate::from_ymd_opt(year, month.number_from_month() - 1, 1)?
65                };
66
67                // Find the last day of the previous month
68                let last_day_of_prev_month = if prev_month.month() == 12 {
69                    // December has 31 days
70                    NaiveDate::from_ymd_opt(prev_month.year(), 12, 31)?
71                } else {
72                    // For other months, the last day is one day before the first of current month
73                    first_of_month - Duration::days(1)
74                };
75
76                // Calculate how many days to go back to find the last Wednesday
77                let days_back = (last_day_of_prev_month.weekday().num_days_from_monday() + 7
78                    - Weekday::Wed.num_days_from_monday())
79                    % 7;
80
81                // Get the last Wednesday
82                return Some(last_day_of_prev_month - Duration::days(days_back as i64));
83            }
84
85            None
86        }
87
88        let instrument_info: ParsedOptionInfo = parse_instrument_name(&raw.instrument_name);
89        let underlying = Some(instrument_info.asset_name);
90        let strike = match instrument_info {
91            ParsedOptionInfo {
92                strike: Some(s), ..
93            } => Some(s.parse::<f64>().ok()).flatten(),
94            _ => None,
95        };
96        let option_type = instrument_info.option_type;
97        let deal_date = NaiveDateTime::parse_from_str(&raw.date_utc, "%Y-%m-%dT%H:%M:%S")
98            .map(|naive| naive.and_utc())
99            .unwrap_or_else(|_| Utc::now());
100        let pnl_eur = raw
101            .profit_and_loss
102            .trim_start_matches('E')
103            .replace(',', "") // Remove comma separators for thousands
104            .parse::<f64>()
105            .unwrap_or(0.0);
106
107        let expiry = parse_period(&raw.period);
108
109        let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < 1.0;
110
111        StoreTransaction {
112            deal_date,
113            underlying,
114            strike,
115            option_type,
116            expiry,
117            transaction_type: raw.transaction_type.clone(),
118            pnl_eur,
119            reference: raw.reference.clone(),
120            is_fee,
121            raw_json: raw.to_string(),
122        }
123    }
124}
125
126impl From<&AccountTransaction> for StoreTransaction {
127    fn from(raw: &AccountTransaction) -> Self {
128        StoreTransaction::from(raw.clone())
129    }
130}
131
132/// Collection of processed transactions from IG Markets
133///
134/// This struct is a wrapper around a vector of `StoreTransaction` objects
135/// and provides convenient methods for accessing and converting transaction data.
136pub struct TransactionList(pub Vec<StoreTransaction>);
137
138impl AsRef<[StoreTransaction]> for TransactionList {
139    fn as_ref(&self) -> &[StoreTransaction] {
140        &self.0[..]
141    }
142}
143
144impl From<&Vec<AccountTransaction>> for TransactionList {
145    fn from(raw: &Vec<AccountTransaction>) -> Self {
146        TransactionList(
147            raw.iter() // Use iter() instead of into_iter() for references
148                .map(StoreTransaction::from) // This assumes there is an impl From<&AccountTransaction> for StoreTransaction
149                .collect(),
150        )
151    }
152}