Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TradeRepublic converter #180

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
next-version: 0.28.2
next-version: 0.29.0
assembly-informational-format: "{NuGetVersion}"
mode: ContinuousDeployment
branches:
Expand Down
54 changes: 31 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This tool allows you to convert CSV transaction exports to an import file that c
- [Saxo](https://www.home.saxo)
- [Schwab](https://www.schwab.com)
- [Swissquote](https://en.swissquote.com/)
- [TradeRepublic](https://traderepublic.com)
- [Trading 212](https://trading212.com)
- [XTB](https://www.xtb.com/int)

Expand Down Expand Up @@ -144,6 +145,12 @@ Login to your Swissquote account. From the bar menu click on "Transactions". Sel

**NOTE:** For Swissquote it's important you have set your display language as English. You can do this by logging into your Swissquote account and then select "My Account" (Mein Konto). Click the "Settings"-button (Einstellungen), then change your display language (displaysprache) to English. After this change, your Swissquote export will be in English.

### TradeRepublic

> **As TradeRepublic itself does not offer CSV exports, this can be achieved using a community tool called [`pytr`](https://github.com/pytr-org/pytr/). The instructions below are based on that tool!**

Download [`pytr`](https://github.com/pytr-org/pytr/) on your local machine. After installing, run `uvx pytr dl_docs ./docs`. This will download all of your transaction files and prepare the files needed to create a CSV. You might need to provide your TradeRepublic credentials to be able to download the documents. After downloading the documents, then convert those to a CSV file like `uvx pytr export_transactions C:\users\you\desktop\docs\all_events.json C:\users\you\desktop\docs\traderepublic.csv`.

### Trading 212

Login to your Trading 212 account and create an export file (via History > Download icon). Choose the period from which you wish to export your history and click download.
Expand Down Expand Up @@ -247,29 +254,30 @@ The repository contains a sample `.env` file. Rename this from `.env.sample`.

You can now run `npm run start [exporttype]`. See the table with run commands below. The tool will open your export and will convert this. It retrieves the symbols that are supported with YAHOO Finance (e.g. for European stocks like `ASML`, it will retrieve `ASML.AS` by the corresponding ISIN).

| Exporter | Run command |
| ------------- | ---------------------------------- |
| Avanza | `run start avanza` |
| Bitvavo | `run start bitvavo` (or `bv`) |
| BUX | `run start bux` |
| Coinbase | `run start coinbase` (or `cb`) |
| CoinTracking | `run start cointracking` (or `ct`) |
| DEGIRO | `run start degiro` |
| Delta | `run start delta` |
| Directa | `run start directa` |
| eToro | `run start etoro` |
| Finpension | `run start finpension` (or `fp`) |
| Freetrade | `run start freetrade` (or `ft`) |
| IBKR | `run start ibkr` |
| Investimental | `run start investimental` |
| Parqet | `run start parqet` |
| Rabobank | `run start rabobank` |
| Revolut | `run start revolut` |
| Saxo | `run start saxo` |
| Schwab | `run start schwab` |
| Swissquote | `run start swissquote` (or `sq`) |
| Trading 212 | `run start trading212` (or `t212`) |
| XTB | `run start xtb` |
| Exporter | Run command |
| ------------- | ----------------------------------- |
| Avanza | `run start avanza` |
| Bitvavo | `run start bitvavo` (or `bv`) |
| BUX | `run start bux` |
| Coinbase | `run start coinbase` (or `cb`) |
| CoinTracking | `run start cointracking` (or `ct`) |
| DEGIRO | `run start degiro` |
| Delta | `run start delta` |
| Directa | `run start directa` |
| eToro | `run start etoro` |
| Finpension | `run start finpension` (or `fp`) |
| Freetrade | `run start freetrade` (or `ft`) |
| IBKR | `run start ibkr` |
| Investimental | `run start investimental` |
| Parqet | `run start parqet` |
| Rabobank | `run start rabobank` |
| Revolut | `run start revolut` |
| Saxo | `run start saxo` |
| Schwab | `run start schwab` |
| Swissquote | `run start swissquote` (or `sq`) |
| TradeRepublic | `run start traderepublic` (or `tr`) |
| Trading 212 | `run start trading212` (or `t212`) |
| XTB | `run start xtb` |

### Caching

Expand Down
Binary file modified assets/social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/social.pxz
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "export-to-ghostfolio",
"version": "0.28.2",
"version": "0.29.0",
"type": "module",
"description": "Convert multiple broker exports to Ghostfolio import",
"scripts": {
Expand Down
7 changes: 7 additions & 0 deletions samples/traderepublic-export.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Datum;Transactietype;Waarde (netto);Opmerking;ISIN;Aantal;Kosten;Belasting
2024-11-30;Onttrekking;-10,84;John Doe;;;;
2024-11-26;Storting;0,01;John Doe;;;;
2024-07-23;Aankoop;-5;FTSE Developed World USD (Dist);IE00BKX55T58;0,052361;;
2024-11-29;Verkoop;10,799;FTSE Developed World USD (Dist);IE00BKX55T58;0,104341;;
2024-07-16;Aankoop;-5;FTSE Developed World USD (Dist);IE00BKX55T58;0,05198;;
2024-09-25;Dividend;0,03;FTSE Developed World USD (Dist);IE00BKX55T58;;;
14 changes: 10 additions & 4 deletions src/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { RevolutConverter } from "./converters/revolutConverter";
import { SaxoConverter } from "./converters/saxoConverter";
import { SchwabConverter } from "./converters/schwabConverter";
import { SwissquoteConverter } from "./converters/swissquoteConverter";
import { TradeRepublicConverter } from "./converters/tradeRepublicConverter";
import { Trading212Converter } from "./converters/trading212Converter";
import { XtbConverter } from "./converters/xtbConverter";

Expand Down Expand Up @@ -147,6 +148,10 @@ async function createConverter(converterType: string, securityService?: Security
console.log("[i] Processing file using Delta converter");
converter = new DeltaConverter(securityService);
break;
case "directa":
console.log("[i] Processing file using Directa converter, this is an experimental converter!");
converter = new DirectaConverter(securityService);
break;
case "etoro":
console.log("[i] Processing file using Etoro converter");
converter = new EtoroConverter(securityService);
Expand Down Expand Up @@ -194,6 +199,11 @@ async function createConverter(converterType: string, securityService?: Security
console.log("[i] Processing file using Swissquote converter");
converter = new SwissquoteConverter(securityService);
break;
case "tr":
case "traderepublic":
console.log("[i] Processing file using TradeRepublic converter");
converter = new TradeRepublicConverter(securityService);
break;
case "t212":
case "trading212":
console.log("[i] Processing file using Trading212 converter");
Expand All @@ -203,10 +213,6 @@ async function createConverter(converterType: string, securityService?: Security
console.log("[i] Processing file using XTB converter");
converter = new XtbConverter(securityService);
break;
case "directa":
console.log("[i] Processing file using Directa converter, this is an experimental converter!");
converter = new DirectaConverter(securityService);
break;
default:
throw new Error(`Unknown converter '${converterType}' provided`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/converters/etoroConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class EtoroConverter extends AbstractConverter {
*/
protected processHeaders(_: string): string[] {

// Generic header mapping from the DEGIRO CSV export.
// Generic header mapping from the Etoro CSV export.
const csvHeaders = [
"date",
"type",
Expand Down
147 changes: 147 additions & 0 deletions src/converters/tradeRepublicConverter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { TradeRepublicConverter } from "./tradeRepublicConverter";
import { SecurityService } from "../securityService";
import { GhostfolioExport } from "../models/ghostfolioExport";
import YahooFinanceServiceMock from "../testing/yahooFinanceServiceMock";

describe("tradeRepublicConverter", () => {

beforeEach(() => {
jest.spyOn(console, "log").mockImplementation(jest.fn());
});

afterEach(() => {
jest.clearAllMocks();
});

it("should construct", () => {

// Act
const sut = new TradeRepublicConverter(new SecurityService(new YahooFinanceServiceMock()));

// Assert
expect(sut).toBeTruthy();
});

it("should process sample CSV file", (done) => {

// Arange
const sut = new TradeRepublicConverter(new SecurityService(new YahooFinanceServiceMock()));
const inputFile = "samples/traderepublic-export.csv";

// Act
sut.readAndProcessFile(inputFile, (actualExport: GhostfolioExport) => {

// Assert
expect(actualExport).toBeTruthy();
expect(actualExport.activities.length).toBeGreaterThan(0);
expect(actualExport.activities.length).toBe(4);

done();
}, () => { done.fail("Should not have an error!"); });
});

describe("should throw an error if", () => {
it("the input file does not exist", (done) => {

// Arrange
const sut = new TradeRepublicConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileName = "tmp/testinput/traderepublic-filedoesnotexist.csv";

// Act
sut.readAndProcessFile(tempFileName, () => { done.fail("Should not succeed!"); }, (err: Error) => {

// Assert
expect(err).toBeTruthy();

done();
});
});

it("the input file is empty", (done) => {

// Arrange
const sut = new TradeRepublicConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileContent = "";
tempFileContent += "Datum;Transactietype;Waarde (netto);Opmerking;ISIN;Aantal;Kosten;Belasting\n";

// Act
sut.processFileContents(tempFileContent, () => { done.fail("Should not succeed!"); }, (err: Error) => {

// Assert
expect(err).toBeTruthy();
expect(err.message).toContain("An error ocurred while parsing");

done();
});
});

it("the header and row column count doesn't match", (done) => {

// Arrange
const sut = new TradeRepublicConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileContent = "";
tempFileContent += "Datum;Transactietype;Waarde (netto);Opmerking;ISIN;Aantal;Kosten;Belasting\n";
tempFileContent += `2024-07-23;Aankoop;-5;FTSE Developed World USD (Dist);IE00BKX55T58;0,052361;;;;`;

// Act
sut.processFileContents(tempFileContent, () => { done.fail("Should not succeed!"); }, (err: Error) => {

// Assert
expect(err).toBeTruthy();
expect(err.message).toBe("An error ocurred while parsing! Details: Invalid Record Length: columns length is 8, got 10 on line 2");

done();
});
});

it("Yahoo Finance throws an error", (done) => {

// Arrange
let tempFileContent = "";
tempFileContent += "Datum;Transactietype;Waarde (netto);Opmerking;ISIN;Aantal;Kosten;Belasting\n";
tempFileContent += `2024-07-23;Aankoop;-5;FTSE Developed World USD (Dist);IE00BKX55T58;0,052361;;`;

// Mock Yahoo Finance service to throw error.
const yahooFinanceServiceMock = new YahooFinanceServiceMock();
jest.spyOn(yahooFinanceServiceMock, "search").mockImplementation(() => { throw new Error("Unit test error"); });
const sut = new TradeRepublicConverter(new SecurityService(yahooFinanceServiceMock));

// Act
sut.processFileContents(tempFileContent, () => { done.fail("Should not succeed!"); }, (err: Error) => {

// Assert
expect(err).toBeTruthy();
expect(err.message).toContain("Unit test error");

done();
});
});
});

it("should log when Yahoo Finance returns no symbol", (done) => {

// Arrange
let tempFileContent = "";
tempFileContent += "Datum;Transactietype;Waarde (netto);Opmerking;ISIN;Aantal;Kosten;Belasting\n";
tempFileContent += `2024-07-23;Aankoop;-5;FTSE Developed World USD (Dist);IE00BKX55T58;0,052361;;`;

// Mock Yahoo Finance service to return no quotes.
const yahooFinanceServiceMock = new YahooFinanceServiceMock();
jest.spyOn(yahooFinanceServiceMock, "search").mockImplementation(() => { return Promise.resolve({ quotes: [] }) });
const sut = new TradeRepublicConverter(new SecurityService(yahooFinanceServiceMock));

// Bit hacky, but it works.
const consoleSpy = jest.spyOn((sut as any).progress, "log");

// Act
sut.processFileContents(tempFileContent, () => {

expect(consoleSpy).toHaveBeenCalledWith("[i] No result found for buy action for IE00BKX55T58! Please add this manually..\n");

done();
}, () => done.fail("Should not have an error!"));
});
});
Loading
Loading