1pub mod money;
2pub mod portfolio;
3
4use anyhow::anyhow;
5use chrono::{NaiveDate, NaiveTime};
6use csv::DeserializeRecordsIter;
7use dateparser::parse;
8use futures::Stream;
9use money::Money;
10use rev_lines::RevLines;
11use serde::{de, Deserialize, Serialize};
12use std::fmt::Write;
13use std::fs::File;
14use std::io::BufReader;
15use std::iter::Peekable;
16use std::pin::Pin;
17use std::task::{Context, Poll};
18
19pub enum TransactionType {
20 Buy,
21 Sell,
22}
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25#[serde(rename_all = "PascalCase")]
26pub struct Transaction {
27 #[serde(deserialize_with = "deserialize_naive_date")]
28 date: NaiveDate,
29 #[serde(deserialize_with = "deserialize_naive_time")]
30 time: NaiveTime,
31 product: String,
32 #[serde(rename(deserialize = "ISIN"))]
33 isin: String,
34 reference: String,
35 quantity: isize,
36 venue: String,
37 price: Money,
38 #[serde(rename(deserialize = "Local value"))]
39 local_value: Money,
40 value: Money,
41 #[serde(rename(deserialize = "Transaction and/or third"))]
42 transaction: Option<String>,
43 #[serde(rename(deserialize = "Exchange rate"))]
44 exchange_rate: Option<String>,
45 total: String,
46 #[serde(rename(deserialize = "Order ID"))]
47 order_id: String,
48}
49
50#[derive(Debug)]
51pub enum TransactionError {
52 SellWithNegPrice { order_id: String },
53 BuyingWithNegPrice { order_id: String },
54}
55
56impl Transaction {
57 pub fn new(
58 date: NaiveDate,
59 isin: String,
60 quantity: isize,
61 value: Money,
62 order_id: String,
63 ) -> Result<Self, TransactionError> {
64 if quantity.is_negative() && value.amount.is_negative() {
65 return Err(TransactionError::SellWithNegPrice { order_id });
66 }
67
68 if quantity.is_positive() && value.amount.is_positive() {
69 return Err(TransactionError::BuyingWithNegPrice { order_id });
70 }
71
72 Ok(Self {
73 date,
74 time: NaiveTime::from_hms(1, 1, 1),
75 product: "".to_string(),
76 isin,
77 reference: "".to_string(),
78 quantity,
79 venue: "".to_string(),
80 price: Default::default(),
81 value,
82 local_value: Default::default(),
83 transaction: None,
84 exchange_rate: None,
85 total: "".to_string(),
86 order_id,
87 })
88 }
89
90 pub fn new_unchecked(
91 date: NaiveDate,
92 isin: String,
93 quantity: isize,
94 value: Money,
95 order_id: String,
96 ) -> Self {
97 match Self::new(date, isin, quantity, value, order_id) {
98 Ok(ok) => ok,
99 Err(e) => panic!("{:#?}", e),
100 }
101 }
102
103 pub fn date(&self) -> &NaiveDate {
104 &self.date
105 }
106
107 pub fn r#type(&self) -> TransactionType {
108 match self.value.amount.is_negative() {
109 true => TransactionType::Buy,
110 false => TransactionType::Sell,
111 }
112 }
113}
114
115fn deserialize_naive_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
116where
117 D: de::Deserializer<'de>,
118{
119 let s: &str = de::Deserialize::deserialize(deserializer)?;
120 let dt = match local_date_parse(s) {
121 Ok(dt) => dt,
122 Err(parse_err) => {
123 let dt = parse(s)
124 .map_err(|e| de::Error::custom(format!("error: {} error: {}", parse_err, e)))?;
125 dt.naive_local().date()
126 }
127 };
128 Ok(dt)
129}
130
131fn deserialize_naive_time<'de, D>(deserializer: D) -> Result<NaiveTime, D::Error>
132where
133 D: de::Deserializer<'de>,
134{
135 let s: &str = de::Deserialize::deserialize(deserializer)?;
136 let nt = match local_time_parse(s) {
137 Ok(dt) => dt,
138 Err(parse_err) => {
139 let dt = parse(s).map_err(|e| de::Error::custom(format!("{}\n{}", parse_err, e)))?;
140 dt.naive_local().time()
141 }
142 };
143 Ok(nt)
144}
145
146fn local_date_parse(s: &str) -> Result<NaiveDate, chrono::ParseError> {
147 NaiveDate::parse_from_str(s, "%d-%m-%Y")
148}
149
150fn local_time_parse(s: &str) -> Result<NaiveTime, chrono::ParseError> {
151 NaiveTime::parse_from_str(s, "%H:%M")
152}
153
154pub struct CsvStream {
155 parser: ReverseCsv,
156}
157
158impl CsvStream {
159 pub fn new(file: File) -> std::io::Result<Self> {
160 let parser = ReverseCsv::new(file)?;
161 Ok(Self { parser })
162 }
163}
164
165impl Stream for CsvStream {
166 type Item = anyhow::Result<Transaction>;
167
168 fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
169 let next = self
170 .as_mut()
171 .parser
172 .next()
173 .map(|res| res.map_err(|e| anyhow!("{}", e)));
174 Poll::Ready(next)
175 }
176}
177
178struct ReverseCsv {
179 rev_lines: Peekable<RevLines<File>>,
180}
181
182impl ReverseCsv {
183 pub fn new(file: File) -> std::io::Result<Self> {
184 let reader = BufReader::new(file);
185 let rev_lines = RevLines::new(reader)?.peekable();
186
187 Ok(Self { rev_lines })
188 }
189}
190
191const HEADERS: &str = "Date,Time,Product,ISIN,Reference,Venue,Quantity,Price,,Local value,,Value,,Exchange rate,Transaction and/or third,,Total,,Order ID\n";
192
193impl Iterator for ReverseCsv {
194 type Item = anyhow::Result<Transaction>;
195
196 fn next(&mut self) -> Option<Self::Item> {
197 let line = self.rev_lines.next()?;
198 self.rev_lines.peek()?;
200
201 let mut input = HEADERS.to_string();
202 input.write_str(&line).unwrap();
203
204 let mut rdr = csv::ReaderBuilder::new()
205 .has_headers(true)
206 .from_reader(input.as_bytes());
207 let mut iter: DeserializeRecordsIter<_, Transaction> = rdr.deserialize();
208
209 let item = iter.next()?;
210
211 assert!(iter.peekable().peek().is_none());
212
213 let res = match item {
214 Ok(k) => anyhow::Result::Ok(k),
215 Err(e) => anyhow::Result::Err(anyhow!("{}", e)),
216 };
217
218 Some(res)
219 }
220}