ig_client/presentation/
transaction.rs1use 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::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8const FEE_THRESHOLD_EUR: f64 = 1.0;
17
18#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, PartialEq, Clone, Default)]
20pub struct StoreTransaction {
21 pub deal_date: DateTime<Utc>,
23 pub underlying: Option<String>,
25 pub strike: Option<f64>,
27 pub option_type: Option<String>,
29 pub expiry: Option<NaiveDate>,
31 pub transaction_type: String,
33 pub pnl_eur: f64,
35 pub reference: String,
37 pub is_fee: bool,
39 pub raw_json: String,
41}
42
43impl From<AccountTransaction> for StoreTransaction {
44 fn from(raw: AccountTransaction) -> Self {
45 fn parse_period(period: &str) -> Option<NaiveDate> {
46 if let Some((day_str, rest)) = period.split_once('-')
48 && let Some((mon_str, year_str)) = rest.split_once('-')
49 {
50 if let Ok(day) = day_str.parse::<u32>() {
52 let month = chrono::Month::from_str(mon_str).ok()?;
53 let year = 2000 + year_str.parse::<i32>().ok()?;
54
55 return NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
57 }
58 }
59
60 if let Some((mon_str, year_str)) = period.split_once('-') {
62 let month = chrono::Month::from_str(mon_str).ok()?;
63 let year = 2000 + year_str.parse::<i32>().ok()?;
64
65 let first_of_month = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1)?;
67
68 let prev_month = if month.number_from_month() == 1 {
70 NaiveDate::from_ymd_opt(year - 1, 12, 1)?
72 } else {
73 NaiveDate::from_ymd_opt(year, month.number_from_month() - 1, 1)?
75 };
76
77 let last_day_of_prev_month = if prev_month.month() == 12 {
79 NaiveDate::from_ymd_opt(prev_month.year(), 12, 31)?
81 } else {
82 first_of_month - Duration::days(1)
84 };
85
86 let days_back = (last_day_of_prev_month.weekday().num_days_from_monday() + 7
88 - Weekday::Wed.num_days_from_monday())
89 % 7;
90
91 return Some(last_day_of_prev_month - Duration::days(days_back as i64));
93 }
94
95 None
96 }
97
98 let instrument_info: ParsedOptionInfo = parse_instrument_name(&raw.instrument_name);
99 let underlying = Some(instrument_info.asset_name);
100 let strike = match instrument_info {
101 ParsedOptionInfo {
102 strike: Some(s), ..
103 } => Some(s.parse::<f64>().ok()).flatten(),
104 _ => None,
105 };
106 let option_type = instrument_info.option_type;
107 let deal_date = NaiveDateTime::parse_from_str(&raw.date_utc, "%Y-%m-%dT%H:%M:%S")
108 .map(|naive| naive.and_utc())
109 .unwrap_or_else(|_| Utc::now());
110 let pnl_eur = raw
114 .profit_and_loss
115 .trim_start_matches('E')
116 .replace(',', "") .parse::<f64>()
118 .unwrap_or(0.0);
119
120 let expiry = parse_period(&raw.period);
121
122 let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < FEE_THRESHOLD_EUR;
123
124 StoreTransaction {
125 deal_date,
126 underlying,
127 strike,
128 option_type,
129 expiry,
130 transaction_type: raw.transaction_type.clone(),
131 pnl_eur,
132 reference: raw.reference.clone(),
133 is_fee,
134 raw_json: raw.to_string(),
135 }
136 }
137}
138
139impl From<&AccountTransaction> for StoreTransaction {
140 fn from(raw: &AccountTransaction) -> Self {
141 StoreTransaction::from(raw.clone())
142 }
143}
144
145pub struct TransactionList(pub Vec<StoreTransaction>);
150
151impl AsRef<[StoreTransaction]> for TransactionList {
152 fn as_ref(&self) -> &[StoreTransaction] {
153 &self.0[..]
154 }
155}
156
157impl From<&Vec<AccountTransaction>> for TransactionList {
158 fn from(raw: &Vec<AccountTransaction>) -> Self {
159 TransactionList(
160 raw.iter() .map(StoreTransaction::from) .collect(),
163 )
164 }
165}