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
8#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, PartialEq, Clone, Default)]
10pub struct StoreTransaction {
11 pub deal_date: DateTime<Utc>,
13 pub underlying: Option<String>,
15 pub strike: Option<f64>,
17 pub option_type: Option<String>,
19 pub expiry: Option<NaiveDate>,
21 pub transaction_type: String,
23 pub pnl_eur: f64,
25 pub reference: String,
27 pub is_fee: bool,
29 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 if let Some((day_str, rest)) = period.split_once('-')
38 && let Some((mon_str, year_str)) = rest.split_once('-')
39 {
40 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 NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
47 }
48 }
49
50 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 let first_of_month = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1)?;
57
58 let prev_month = if month.number_from_month() == 1 {
60 NaiveDate::from_ymd_opt(year - 1, 12, 1)?
62 } else {
63 NaiveDate::from_ymd_opt(year, month.number_from_month() - 1, 1)?
65 };
66
67 let last_day_of_prev_month = if prev_month.month() == 12 {
69 NaiveDate::from_ymd_opt(prev_month.year(), 12, 31)?
71 } else {
72 first_of_month - Duration::days(1)
74 };
75
76 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 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(',', "") .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
132pub 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() .map(StoreTransaction::from) .collect(),
150 )
151 }
152}