Skip to content

Commit 78f63dd

Browse files
author
Jan Bongers
committed
Add Trade Republic English Account statement Parser
1 parent fa00d98 commit 78f63dd

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed

GhostfolioSidekick/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ internal static IHostBuilder CreateHostBuilder()
131131
services.AddScoped<IFileImporter, TradeRepublicInvoiceParserNL>();
132132
services.AddScoped<IFileImporter, TradeRepublicInvoiceParserDE>();
133133
services.AddScoped<IFileImporter, TradeRepublicStatementParserNL>();
134+
services.AddScoped<IFileImporter, TradeRepublicStatementParserEN>();
134135
services.AddScoped<IFileImporter, Trading212Parser>();
135136

136137
services.AddScoped<IHoldingStrategy, AddStakeRewardsToPreviousBuyActivity>();
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
using GhostfolioSidekick.Model;
2+
using GhostfolioSidekick.Model.Activities;
3+
using GhostfolioSidekick.Parsers.PDFParser;
4+
using GhostfolioSidekick.Parsers.PDFParser.PdfToWords;
5+
using System.Globalization;
6+
7+
namespace GhostfolioSidekick.Parsers.TradeRepublic
8+
{
9+
public class TradeRepublicStatementParserEN : PdfBaseParser
10+
{
11+
private const string Keyword_Datum = "DATE";
12+
private const string Keyword_Type = "TYPE";
13+
private const string Keyword_Beschrijving = "DESCRIPTION";
14+
private const string Keyword_BedragBij = "MONEY IN";
15+
private const string Keyword_BedragAf = "MONEY OUT";
16+
private const string Keyword_Saldo = "BALANCE";
17+
18+
private List<string> TableKeyWords
19+
{
20+
get
21+
{
22+
return [
23+
Keyword_Datum,
24+
Keyword_Type,
25+
Keyword_Beschrijving,
26+
Keyword_BedragBij,
27+
Keyword_BedragAf,
28+
Keyword_Saldo
29+
];
30+
}
31+
}
32+
33+
public TradeRepublicStatementParserEN(IPdfToWordsParser parsePDfToWords) : base(parsePDfToWords)
34+
{
35+
}
36+
37+
protected override bool CanParseRecords(List<SingleWordToken> words)
38+
{
39+
var foundTradeRepublic = false;
40+
var foundStatement = false;
41+
42+
for (int i = 0; i < words.Count; i++)
43+
{
44+
if (IsCheckWords("Trade Republic Bank GmbH", words, i))
45+
{
46+
foundTradeRepublic = true;
47+
}
48+
49+
if (
50+
IsCheckWords("ACCOUNT TRANSACTIONS", words, i))
51+
{
52+
foundStatement = true;
53+
}
54+
}
55+
56+
return foundTradeRepublic && foundStatement;
57+
}
58+
59+
protected override List<PartialActivity> ParseRecords(List<SingleWordToken> words)
60+
{
61+
var activities = new List<PartialActivity>();
62+
63+
// detect headers
64+
var headers = new List<MultiWordToken>();
65+
66+
bool inHeader = false;
67+
68+
for (int i = 0; i < words.Count; i++)
69+
{
70+
var word = words[i];
71+
72+
if (headers.Count == TableKeyWords.Count) // parsing rows
73+
{
74+
var incr = ParseActivity(words, i, activities);
75+
if (incr == int.MaxValue)
76+
{
77+
break;
78+
}
79+
80+
#pragma warning disable S127 // "for" loop stop conditions should be invariant
81+
i += incr;
82+
#pragma warning restore S127 // "for" loop stop conditions should be invariant
83+
}
84+
85+
if (Keyword_Datum == word.Text) // start of header
86+
{
87+
inHeader = true;
88+
}
89+
90+
if (inHeader) // add column headers
91+
{
92+
var matched = false;
93+
foreach (var kw in TableKeyWords)
94+
{
95+
var keywordMatch = true;
96+
string[] keywordSplitted = kw.Split(" ");
97+
for (int j = 0; j < keywordSplitted.Length; j++)
98+
{
99+
string? keyword = keywordSplitted[j];
100+
if (words[i + j].Text != keyword)
101+
{
102+
keywordMatch = false;
103+
break;
104+
}
105+
}
106+
107+
if (keywordMatch)
108+
{
109+
headers.Add(new MultiWordToken(kw, word.BoundingBox));
110+
matched = true;
111+
#pragma warning disable S127 // "for" loop stop conditions should be invariant
112+
i += keywordSplitted.Length - 1;
113+
#pragma warning restore S127 // "for" loop stop conditions should be invariant
114+
break;
115+
}
116+
}
117+
118+
if (!matched)
119+
{
120+
inHeader = false;
121+
headers.Clear();
122+
}
123+
}
124+
125+
if (Keyword_Saldo == word.Text) // end of header
126+
{
127+
inHeader = false;
128+
}
129+
}
130+
131+
return activities;
132+
}
133+
134+
private static int ParseActivity(List<SingleWordToken> words, int i, List<PartialActivity> activities)
135+
{
136+
for (int j = i; j < words.Count - 2; j++)
137+
{
138+
CultureInfo dutchCultureInfo = new CultureInfo("en-US");
139+
if (DateTime.TryParseExact(
140+
words[j].Text + " " + words[j + 1].Text + " " + words[j + 2].Text,
141+
["dd MMM yyyy", "dd MMM. yyyy"],
142+
dutchCultureInfo,
143+
DateTimeStyles.AssumeUniversal,
144+
out var date))
145+
{
146+
// start of a new activity
147+
SingleWordToken singleWordToken = words[j + 3];
148+
if (singleWordToken.Text == "Trade" || singleWordToken.Text == "Earnings" || singleWordToken.Text == "Page")
149+
{
150+
return j - i + 3;
151+
}
152+
153+
var items = words.Skip(j + 4).TakeWhile(w => w.BoundingBox!.Row == singleWordToken.BoundingBox!.Row).ToList();
154+
155+
var amountText = items[items.Count - 2];
156+
var currency = new Model.Currency(CurrencyTools.GetCurrencyFromSymbol(amountText.Text.Substring(0, 1)));
157+
var amount = decimal.Parse(amountText.Text.Substring(1).Trim(), dutchCultureInfo);
158+
159+
var id = $"Trade_Republic_{singleWordToken.Text}_{date.ToInvariantDateOnlyString()}";
160+
161+
switch (singleWordToken.Text)
162+
{
163+
case "Interest Payment":
164+
activities.Add(PartialActivity.CreateInterest(
165+
currency,
166+
date,
167+
amount,
168+
string.Join(" ", items.Take(items.Count - 2)),
169+
new Money(currency, amount),
170+
id));
171+
172+
break;
173+
case "Transfer":
174+
activities.Add(PartialActivity.CreateCashDeposit(
175+
currency,
176+
date,
177+
amount,
178+
new Money(currency, amount),
179+
id));
180+
break;
181+
case "Card Transaction":
182+
activities.Add(PartialActivity.CreateCashWithdrawal(
183+
currency,
184+
date,
185+
amount,
186+
new Money(currency, amount),
187+
id));
188+
break;
189+
case "Reward":
190+
activities.Add(PartialActivity.CreateGift(
191+
currency,
192+
date,
193+
amount,
194+
new Money(currency, amount),
195+
id));
196+
break;
197+
case "Trade":
198+
// Buy or sell
199+
// Should be handeld by another parser
200+
break;
201+
case "Referral":
202+
activities.Add(PartialActivity.CreateGift(
203+
currency,
204+
date,
205+
amount,
206+
new Money(currency, amount),
207+
id));
208+
break;
209+
case "Earnings":
210+
// Dividend
211+
// Should be handeld by another parser
212+
break;
213+
default:
214+
throw new NotSupportedException();
215+
}
216+
217+
return j - i + 3 + items.Count;
218+
}
219+
}
220+
221+
return int.MaxValue;
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)