ig_client/application/models/
transaction.rs1use 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#[derive(Debug, Serialize, Deserialize)]
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_json_display!(StoreTransaction);
34
35impl From<AccountTransaction> for StoreTransaction {
36 fn from(raw: AccountTransaction) -> Self {
37 fn parse_period(period: &str) -> Option<NaiveDate> {
38 if let Some((day_str, rest)) = period.split_once('-') {
40 if let Some((mon_str, year_str)) = rest.split_once('-') {
41 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 NaiveDate::from_ymd_opt(year, month.number_from_month(), day);
48 }
49 }
50 }
51
52 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 let first_of_month = NaiveDate::from_ymd_opt(year, month.number_from_month(), 1)?;
59
60 let prev_month = if month.number_from_month() == 1 {
62 NaiveDate::from_ymd_opt(year - 1, 12, 1)?
64 } else {
65 NaiveDate::from_ymd_opt(year, month.number_from_month() - 1, 1)?
67 };
68
69 let last_day_of_prev_month = if prev_month.month() == 12 {
71 NaiveDate::from_ymd_opt(prev_month.year(), 12, 31)?
73 } else {
74 first_of_month - Duration::days(1)
76 };
77
78 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 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 .parse::<f64>()
106 .unwrap_or(0.0);
107
108 let expiry = parse_period(&raw.period);
109
110 let is_fee = raw.transaction_type == "WITH" && pnl_eur.abs() < 1.0;
111
112 StoreTransaction {
113 deal_date,
114 underlying,
115 strike,
116 option_type,
117 expiry,
118 transaction_type: raw.transaction_type.clone(),
119 pnl_eur,
120 reference: raw.reference.clone(),
121 is_fee,
122 raw_json: raw.to_string(),
123 }
124 }
125}
126
127impl From<&AccountTransaction> for StoreTransaction {
128 fn from(raw: &AccountTransaction) -> Self {
129 StoreTransaction::from(raw.clone())
130 }
131}
132
133pub struct TransactionList(pub Vec<StoreTransaction>);
138
139impl AsRef<[StoreTransaction]> for TransactionList {
140 fn as_ref(&self) -> &[StoreTransaction] {
141 &self.0[..]
142 }
143}
144
145impl From<&Vec<AccountTransaction>> for TransactionList {
146 fn from(raw: &Vec<AccountTransaction>) -> Self {
147 TransactionList(
148 raw.iter() .map(StoreTransaction::from) .collect(),
151 )
152 }
153}