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 Saxo converter #160

Open
wants to merge 12 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.22.4
next-version: 0.23.0
assembly-informational-format: "{NuGetVersion}"
mode: ContinuousDeployment
branches:
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/dickwolff)  
[![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/dickw0lff)

![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/dickwolff/dd5dc24ffa62de59b3d836f856f48a10/raw/cov.json)
[![Docker Pulls](https://img.shields.io/docker/pulls/dickwolff/export-to-ghostfolio?style=for-the-badge)](https://hub.docker.com/r/dickwolff/export-to-ghostfolio)   ![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/dickwolff/dd5dc24ffa62de59b3d836f856f48a10/raw/cov.json)   ![Stars](https://img.shields.io/github/stars/dickwolff/export-to-ghostfolio?style=for-the-badge)

This tool allows you to convert a multiple transaction exports (CSV) to an import file that can be read by [Ghostfolio](https://github.com/ghostfolio/ghostfolio/). Currently there is support for:
This tool allows you to convert CSV transaction exports to an import file that can be read by [Ghostfolio](https://github.com/ghostfolio/ghostfolio/). Currently there is support for 18 brokers:

- [Bitvavo](https://bitvavo.com)
- [BUX](https://bux.com)
Expand All @@ -20,6 +20,7 @@ This tool allows you to convert a multiple transaction exports (CSV) to an impor
- [Parqet](https://www.parqet.com/)
- [Rabobank](https://rabobank.nl)
- [Revolut](https://revolut.com)
- [Saxo](https://www.home.saxo/nl-nl)
- [Schwab](https://www.schwab.com)
- [Swissquote](https://en.swissquote.com/)
- [Trading 212](https://trading212.com)
Expand Down Expand Up @@ -110,6 +111,10 @@ Login to Rabobank and navigate to your investments. Navigate to "Transactions &

Open the Revolut app and open the "Invest"-tab. Press the "More"-button, and then choose "Documents". Select your investment account and select the first option, "Account statement". Choose the "Excel" option and select the date range. Then download the file and save it on your device. Convert the file from `.xlsx` to `.csv`. **Set the separation character to `,` (comma)!**

### Saxo

Login to your Saxo account. Go to your profile (button on the top-right), then choose "Transaction overview" (near the bottom). Click the blue "Export"-button and click the "Excel"-button to download the file and save it on your device. Convert the file from `.xlsx` to `.csv`. **Set the separation character to `,` (comma)!**

### Schwab

Login to your Schwab account. Go to "Accounts" then "History". Select the account you want to download details from. Select the "Date Range" and select "Export" (csv). Save the file.
Expand Down Expand Up @@ -236,9 +241,10 @@ You can now run `npm run start [exporttype]`. See the table with run commands be
| Freetrade | `run start freetrade` (or `ft`) |
| IBKR | `run start ibkr` |
| Investimental | `run start investimental` |
| Parqet | `run start pareqt` |
| 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`) |
Expand Down
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.22.4",
"version": "0.23.0",
"type": "module",
"description": "Convert multiple broker exports to Ghostfolio import",
"scripts": {
Expand Down
29 changes: 29 additions & 0 deletions samples/saxo-export.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Client ID,Trade Date,Value Date,Type,Instrument,Instrument ISIN,Instrument currency,Exchange Description,Instrument Symbol,Event,Amount,Order ID,Conversion Rate
,02-Jan-2025,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Sell 3 @ 139.74 USD,"419,22",,1
,02-Jan-2025,02-Jan-2025,Cash amount,,,USD,,,Custody Fee,"-3,91",,1
,02-Jan-2025,02-Jan-2025,Cash amount,,,USD,,,Custody Fee,"-5,2",,1
,02-Jan-2025,02-Jan-2025,Cash amount,,,USD,,,Custody Fee,"-2,83",,1
,02-Jan-2025,02-Jan-2025,Cash amount,,,USD,,,Custody Fee,"-0,64",,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,500,,1
,30-Dec-2024,31-Dec-2024,Trade,NVIDIA Corp.,US67066G1040,USD,NASDAQ,NVDA:xnas,Buy 3 @ 134.85 USD,"-405,55",,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,833,,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,833,,1
,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1
,30-Dec-2024,31-Dec-2024,Trade,Meta Platforms Inc.,US30303M1027,USD,NASDAQ,META:xnas,Buy 1 @ 589.16 USD,"-590,16",,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,500,,1
,30-Dec-2024,31-Dec-2024,Trade,Amazon.com Inc.,US0231351067,USD,NASDAQ,AMZN:xnas,Buy 2 @ 220.00 USD,-441,,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,500,,1
,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1
,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 17 @ 139.74 USD,"-2379,35",,1
,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1
,30-Dec-2024,30-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,833,,1
,23-Dec-2024,23-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,750,,1
,23-Dec-2024,23-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,750,,1
,16-Dec-2024,18-Dec-2024,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 2 @ 142.58 USD,"-288,96",,1
,16-Dec-2024,16-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,280,,1
,16-Dec-2024,16-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,280,,1
,16-Dec-2024,16-Dec-2024,Cash Transfer,,,USD,Unknown,,Deposit,280,,1
,16-Dec-2024,18-Dec-2024,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 2 @ 142.58 USD,"-288,96",,1
,16-Dec-2024,18-Dec-2024,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 2 @ 142.58 USD,"-288,96",,1
,16-jan-2025,20-jan-2025,Transactie,Vanguard S&P 500 Dist UCITS ETF,IE00B3XXRP09,EUR,Euronext Amsterdam,VUSA:xams,Koop 1 @ 110.01 EUR,-110,,1
,16-Dec-2024,16-Dec-2024,Cash Transfer,,,USD,Unknown,,Withdrawal,-280,,1
7 changes: 6 additions & 1 deletion src/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DeGiroConverter } from "./converters/degiroConverter";
import { DeGiroConverterV2 } from "./converters/degiroConverterV2";
import { DeGiroConverterV3 } from "./converters/degiroConverterV3";
import { DeltaConverter } from "./converters/deltaConverter";
import { DirectaConverter } from "./converters/directaConverter";
import { EtoroConverter } from "./converters/etoroConverter";
import { FinpensionConverter } from "./converters/finpensionConverter";
import { FreetradeConverter } from "./converters/freetradeConverter";
Expand All @@ -19,11 +20,11 @@ import { InvestimentalConverter } from "./converters/investimentalConverter";
import { ParqetConverter } from "./converters/parqetConverter";
import { RabobankConverter } from "./converters/rabobankConverter";
import { RevolutConverter } from "./converters/revolutConverter";
import { SaxoConverter } from "./converters/saxoConverter";
import { SchwabConverter } from "./converters/schwabConverter";
import { SwissquoteConverter } from "./converters/swissquoteConverter";
import { Trading212Converter } from "./converters/trading212Converter";
import { XtbConverter } from "./converters/xtbConverter";
import { DirectaConverter } from "./converters/directaConverter";

import packageInfo from "../package.json";

Expand Down Expand Up @@ -163,6 +164,10 @@ async function createConverter(converterType: string, securityService?: Security
console.log("[i] Processing file using Revolut converter");
converter = new RevolutConverter(securityService);
break;
case "saxo":
console.log("[i] Processing file using Saxo converter");
converter = new SaxoConverter(securityService);
break;
case "schwab":
console.log("[i] Processing file using Schwab converter");
converter = new SchwabConverter(securityService);
Expand Down
147 changes: 147 additions & 0 deletions src/converters/saxoConverter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { SaxoConverter } from "./saxoConverter";
import { SecurityService } from "../securityService";
import { GhostfolioExport } from "../models/ghostfolioExport";
import YahooFinanceServiceMock from "../testing/yahooFinanceServiceMock";

describe("saxoConverter", () => {

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

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

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

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

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

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

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

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

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

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 SaxoConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileName = "tmp/testinput/saxo-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 SaxoConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileContent = "";
tempFileContent += `Client ID,Trade Date,Value Date,Type,Instrument,Instrument ISIN,Instrument currency,Exchange Description,Instrument Symbol,Event,Amount,Order ID,Conversion Rate\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 SaxoConverter(new SecurityService(new YahooFinanceServiceMock()));

let tempFileContent = "";
tempFileContent += `Client ID,Trade Date,Value Date,Type,Instrument,Instrument ISIN,Instrument currency,Exchange Description,Instrument Symbol,Event,Amount,Order ID,Conversion Rate\n`;
tempFileContent += `,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1,,`;

// 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 13, got 15 on line 2");

done();
});
});

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

// Arrange
let tempFileContent = "";
tempFileContent += `Client ID,Trade Date,Value Date,Type,Instrument,Instrument ISIN,Instrument currency,Exchange Description,Instrument Symbol,Event,Amount,Order ID,Conversion Rate\n`;
tempFileContent += `,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,VWRA,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1`;

// 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 SaxoConverter(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 += `Client ID,Trade Date,Value Date,Type,Instrument,Instrument ISIN,Instrument currency,Exchange Description,Instrument Symbol,Event,Amount,Order ID,Conversion Rate\n`;
tempFileContent += `,30-Dec-2024,02-Jan-2025,Trade,Vanguard FTSE All-World UCITS ETF,IE00BK5BQT80,USD,London Stock Exchange (ETFs),VWRA:xlon,Buy 3 @ 139.74 USD,"-422,99",,1`;

// Mock Yahoo Finance service to return no quotes.
const yahooFinanceServiceMock = new YahooFinanceServiceMock();
jest.spyOn(yahooFinanceServiceMock, "search").mockImplementation(() => { return Promise.resolve({ quotes: [] }) });
const sut = new SaxoConverter(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 VWRA with currency USD! Please add this manually..\n");

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