Skip to content

Add support for Scalable Capital #272

@ccoles146

Description

@ccoles146

Describe the converter
Scalable Capital is a popular investment platform in Europe particularly Germany.

Provide example input

date;time;status;reference;description;assetType;type;isin;shares;price;amount;fee;tax;currency
2025-09-25;02:00:00;Executed;"WWEK 51597383";"iShares Core FTSE 100 (Dist)";Cash;Distribution;IE0005042456;;;56,78;0,00;0,00;EUR
2025-09-24;02:00:00;Executed;"WWEK 51496442";"iShares European Property Yield (Dist)";Cash;Distribution;IE00B0M63284;;;54,42;0,00;0,00;EUR
2025-09-08;02:00:00;Executed;"6ZIYZYKQGUNM997DUTGZDU";"Scalable Capital Broker Auszahlung";Cash;Withdrawal;;;;-5.743,51;0,00;;EUR
2025-08-01;14:11:01;Executed;"SCALT68r1eJqD4Q";"iShares BIC 50 (Dist)";Security;Savings plan;IE00B1W57M07;3,505;21,395;-74,989475;0,00;0,00;EUR
2025-08-01;14:06:16;Executed;"SCALsyoLeCvmhZY";"iShares Core S&P 500 (Acc)";Security;Savings plan;IE00B5BMR087;0,127;588,66;-74,75982;0,00;0,00;EUR
2025-08-01;14:00:31;Executed;"SCAL3X897nFcRb1";"iShares Physical Silver ETC (Acc)";Security;Savings plan;IE00B4NCWG09;2,459;30,492;-74,979828;0,00;0,00;EUR
2025-08-01;13:55:31;Executed;"SCALdr2xqSqkvTg";"Vanguard U.K. Gilt EUR Hedged (Acc)";Security;Savings plan;IE00BMX0B524;3,738;20,062;-74,991756;0,00;0,00;EUR
2025-08-01;13:48:02;Executed;"SCALLrwijTstUsk";"iShares European Property Yield (Dist)";Security;Savings plan;IE00B0M63284;3,301;30,285;-99,970785;0,00;0,00;EUR
2025-08-01;13:37:31;Executed;"SCALd4viPY9YjHP";"iShares Physical Gold ETC";Security;Savings plan;IE00B4ND3602;1,334;56,215;-74,99081;0,00;0,00;EUR
2025-08-01;13:32:02;Executed;"SCAL5Pr2L8GTuh6";"iShares Core FTSE 100 (Dist)";Security;Savings plan;IE0005042456;7,335;10,224;-74,99304;0,00;0,00;EUR
2025-07-31;02:00:00;Executed;"7BBVUKMMUGHKMHK1SZEZ3E";"Scalable Capital Broker 7x Sparplan";Cash;Deposit;;;;550,00;0,00;;EUR
2025-07-01;14:49:02;Executed;"SCALRH11T7CRP3d";"iShares European Property Yield (Dist)";Security;Savings plan;IE00B0M63284;3,161;31,63;-99,98243;0,00;0,00;EUR
2025-07-01;14:46:43;Executed;"SCAL4Nhbtu8Nz97";"iShares Physical Gold ETC";Security;Savings plan;IE00B4ND3602;1,358;55,22;-74,98876;0,00;0,00;EUR
2024-01-19;01:00:00;Executed;"WWEK 21704483";"iShares Core S&P 500 UCITS ETF";Cash;Taxes;IE00B5BMR087;;;-13,01;0,00;;EUR
2024-01-17;01:00:00;Executed;"WWEK 21365638";"AMUNDI STOXX EUROPE 600 ENERGY ESG SCREENED UCITS ETF Acc";Cash;Taxes;LU1834988278;;;-0,65;0,00;;EUR
2024-01-16;01:00:00;Executed;"WWEK 21265133";"Vanguard Funds PLC - Vanguard U.K. Gilt UCITS ETF - EUR Hedged Accumulating";Cash;Taxes;IE00BMX0B524;;;-14,14;0,00;;EUR
2024-01-02;11:52:51;Executed;"SCALWPVG9cFsACP";"iShares Core S&P 500 UCITS ETF";Security;Savings plan;IE00B5BMR087;0,164;454,57;-74,5494;0,00;0,00;EUR
2024-01-02;11:51:01;Executed;"SCAL6NQA2z6V6Qy";"iShares Core FTSE 100 UCITS ETF GBP (Dist)";Security;Savings plan;IE0005042456;8,631;8,689;-74,9947;0,00;0,00;EUR
2024-01-02;11:43:56;Executed;"SCALGfErMRLsVPj";"iShares Physical Gold ETC";Security;Savings plan;IE00B4ND3602;2,043;36,696;-74,9699;0,00;0,00;EUR
2024-01-02;11:42:33;Executed;"SCALmvVj7pxWVuQ";"iShares European Property Yield UCITS ETF EUR (Dist)";Security;Savings plan;IE00B0M63284;3,358;29,775;-99,9844;0,00;0,00;EUR
2024-01-02;11:39:31;Executed;"SCALjHaQtFWXig8";"iShares BRIC 50 UCITS ETF USD (Dist)";Security;Savings plan;IE00B1W57M07;4,567;16,42;-74,9901;0,00;0,00;EUR
2024-01-02;11:39:02;Executed;"SCAL1FxyzRHiJyk";"iShares Physical Silver ETC";Security;Savings plan;IE00B4NCWG09;3,59;20,89;-74,995;0,00;0,00;EUR
2024-01-02;11:18:01;Executed;"SCALMqCC2gCjoEk";"Vanguard Funds PLC - Vanguard U.K. Gilt UCITS ETF - EUR Hedged Accumulating";Security;Savings plan;IE00BMX0B524;3,573;20,986;-74,9829;0,00;0,00;EUR
2024-01-02;01:00:00;Executed;"WWEK 20270739";"iShares Core FTSE 100 UCITS ETF GBP (Dist)";Cash;Distribution;IE0005042456;;;29,27;0,00;6,63;EUR
2023-12-29;01:00:00;Executed;"GZP7PF2IENSYKIAYHYCAQS";"Scalable Capital Broker 7x Sparplan";Cash;Deposit;;;;550,00;0,00;;EUR
2023-12-29;01:00:00;Executed;"WWEK 20261523";"iShares European Property Yield UCITS ETF EUR (Dist)";Cash;Distribution;IE00B0M63284;;;9,56;0,00;3,41;EUR
2023-12-01;14:56:21;Executed;"SCALY6GDNTUTTsm";"iShares Core S&P 500 UCITS ETF";Security;Savings plan;IE00B5BMR087;0,17;439,63;-74,7371;0,00;0,00;EUR

Additional context
I've already made an attempt to create the converter but I don't quite understand all the operations, I have simply looked at similar ones and tried to recreate it based on how they work. I'm maybe also missing key details about how Ghostfolio works. For example...
There is an asset type category which in my example has values of "Cash" and "Security" if it is cash then it relates to the cash account and if it is Security then it relates to the purchase or sale of a Security. Then there is a "Type" column that determines what the transaction was and this includes "buy", "sell" and "savings plan". In general the transaction can be seen with respect to the Cash account, if the value is negative then it is the purchase of something that used cash for the transaction. If the value is positive then it is a sale that resulted in a deposit to the cash account. A deposit is a transfer in to the cash account from another bank account and a withdrawal is a transfer out of the cash account to another bank account. Also there is a distribution which seems to come into the cash account as a result of owning a particular stock, I think that's the same as a dividend. And there is a separate line for tax which is taken directly from the cash account.

Below I attach the code that I have written:

scalableCapitalRecord.ts

export class ScalableCapitalRecord {
    date: Date;
    time: string;
    status: string;
    reference: string;
    description: string;
    assetType: string;
    type: string;
    isin: string;
    shares: number;
    price: number;
    amount: number;
    fee: number;
    tax: number;
    currency: string;
}

scalableCapitalConverter.ts

import dayjs from "dayjs";
import { parse } from "csv-parse";
import { SecurityService } from "../securityService";
import { ScalableCapitalRecord } from "../models/scalableCapitalRecord";
import { AbstractConverter } from "./abstractconverter";
import { GhostfolioExport } from "../models/ghostfolioExport";
import YahooFinanceRecord from "../models/yahooFinanceRecord";
import { GhostfolioOrderType } from "../models/ghostfolioOrderType";

export class ScalableCapitalConverter extends AbstractConverter {

    constructor(securityService: SecurityService) {
        super(securityService);
    }

    /**
     * @inheritdoc
     */
    public processFileContents(input: string, successCallback: any, errorCallback: any): void {

        // Parse the CSV and convert to Ghostfolio import format.
        parse(input, {
            delimiter: ";",
            fromLine: 2,
            columns: this.processHeaders(input),
            cast: (columnValue, context) => {
                // Custom mapping below.

                // Convert Types to Ghostfolio type
                if (context.column === "type") {
                    const action = columnValue.toLocaleLowerCase();

                    if (action.indexOf("buy") > -1) {
                        return "buy";
                    }
                    else if (action.indexOf("sell") > -1) {
                        return "sell";
                    }
                    else if (action.indexOf("staking") > -1) {
                        return "interest";
                    }
                    else if (action.indexOf("savings plan") > -1) {
                        return "buy";
                    }
                }


                // Parse numbers to floats (from string).
                if (context.column === "price" ||
                    context.column === "shares" ||
                    context.column === "amount" ||
                    context.column === "tax" ||
                    context.column === "fee") {

                    return parseFloat(columnValue.replace(",", "."));
                }

                return columnValue;
            }
        }, async (err, records: ScalableCapitalRecord[]) => {

            try {

                // Check if parsing failed..
                if (err || records === undefined || records.length === 0) {
                    let errorMsg = "An error occurred while parsing!";

                    if (err) {
                        errorMsg += ` Details: ${err.message}`
                    }

                    return errorCallback(new Error(errorMsg))
                }

                console.log("[i] Read CSV file. Start processing..");
                const result: GhostfolioExport = {
                    meta: {
                        date: new Date(),
                        version: "v0"
                    },
                    activities: []
                }

                // Populate the progress bar.
                const bar1 = this.progress.create(records.length, 0);

                for (let idx = 0; idx < records.length; idx++) {
                    const record = records[idx];

                    let security: YahooFinanceRecord;
                    try {
                        security = await this.securityService.getSecurity(
                            record.identifier,
                            null,
                            record.isin,
                            record.currency ?? record.originalCurrency,
                            this.progress);
                    }
                    catch (err) {
                        this.logQueryError(record.identifier || record.holdingName, idx + 2);
                        return errorCallback(err);
                    }

                    // Log whenever there was no match found.
                    if (!security) {
                        this.progress.log(`[i] No result found for ${record.type.toLocaleLowerCase()} action for ${record.identifier || record.holdingName}! Please add this manually..\n`);
                        bar1.increment();
                        continue;
                    }

                    const fees = Math.abs(record.tax) + Math.abs(record.fee);
                    const date = dayjs(`${record.date} ${record.time}`, "YYYY-MM-DD HH:mm:ss"); 

                    // Add record to export.
                    result.activities.push({
                        accountId: process.env.GHOSTFOLIO_ACCOUNT_ID,
                        comment: null,
                        fee: fees,
                        quantity: record.shares,
                        type: GhostfolioOrderType[record.type.toLocaleLowerCase()],
                        unitPrice: record.price,
                        currency: record.currency ?? record.originalCurrency,
                        dataSource: "YAHOO",
                        date: date.format("YYYY-MM-DDTHH:mm:ssZ"),
                        symbol: security.symbol
                    });

                    bar1.increment();
                }

                this.progress.stop();

                successCallback(result);
            }
            catch (error) {
                console.log("[e] An error occurred while processing the file contents. Stack trace:");
                console.log(error.stack);
                this.progress.stop();
                errorCallback(error);
            }
        });
    }

    /**
      * @inheritdoc
      */
    protected processHeaders(_: string): string[] {

        // Generic header mapping from the ScalableCapital CSV export.
        const csvHeaders = [
            "date",
            "time",
            "status",
            "reference",
            "description",
            "assetType",
            "type",
            "isin",
            "shares",
            "price",
            "amount",
            "fee",
            "tax",
            "currency"];

        return csvHeaders;
    }

    /**
     * @inheritdoc
     * 
     * Ignore this method, as there are no ignored records for ScalableCapital.
     */
    /* istanbul ignore next */
    public isIgnoredRecord(record: ScalableCapitalRecord): boolean {
        let ignoredRecordTypes = ["Deposit", "Withdrawal"];

        return ignoredRecordTypes.some(t => record.type.toLocaleLowerCase().indexOf(t) > -1)
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    New brokerRequest support for a new broker

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions