VIAC Invest in Portfolio Performance einbinden

Hallo zusammen

Ich verfasse diesen Beitrag bewusst auf Deutsch, da meine Frage spezifisch VIAC betrifft.

Bisher hatte ich ein VIAC 3a-Konto, das ich problemlos in Portfolio Performance (PP) integriert habe. Ich habe dazu ein neues Depot mit dem Namen “VIAC 3a” erstellt und konnte alle Wertschriften problemlos finden (z. B. VIAC: SWC (CH) IPF I IEF Japan NT).

Nun gibt es seit “Kurzem” VIAC Invest, und ich habe dort ein Depot eröffnet. Natürlich möchte ich dieses nun ebenfalls in PP einbinden. Ich habe bereits ein neues Depot mit dem Namen “VIAC Invest” erstellt und versucht, die PDFs zu importieren, um die Wertpapierkäufe zu dokumentieren – jedoch ohne Erfolg.

Anschliessend habe ich versucht, die Wertschriften manuell zu finden, aber auch das war nicht möglich. Ist jemand bereits auf dieses Problem gestossen?

Hier ein Beispiel für eine VIAC Invest-Wertschrift, die ich nicht in PP finden kann:
VIAC Equity Emerging Markets, ISIN: CH1336969105.

Vielen Dank im Voraus!

Du kannst über die Watchlist üben rechts über das Plus Symbol leere Wertpapiere anlegen, ohne Suchfunktion.

Das bedeutet also, dass der Kurs nicht getrackt wird und ich somit gar nicht nachvollziehen kann, wie sich meine Investition entwickelt?

Es wird schwierig etwas zu tracken was scheinbar nicht an der Börse gehandelt wird.

Du kannst die historischen Kurse von hier via csv importieren: VIAC Equity Emering Markets | Swiss Fund Data
auf dieser Webseite findest du alle Titel mit der ISIN Nummer.

1 Like

Wenn der Kurs nicht über PP abgerufen werden kann, kannst du das Wertpapier selber anlegen und die historischen Kurse manuell eintragen. Entweder einzeln zu bestimmten Zeiten oder dann über einen manuellen CSV-Import von den SwissFundData.

Hey Leute

Da ich auch die Herausforderung mit den Kursen für die neuen Viac Invest Fonds habe greife ich diesen Post gerne auf.

Bei SwissFundData kann man über einen Endpunkt die Kurshistorie als Excel herunterladen für eine Vielzahl an Fonds. Es braucht einfach die ID am Ende, welche SwissFundData definiert hat. Die ID sieht man in der URL wenn man einen Fonds sucht und die Detailseite öffnet.

Bei SwissFundData sind Daten für mehr als 50’000 Fonds & ETFs verfügbar. Insofern wäre es eine umfangreiche Quelle, welche fix in PP eingebaut werden könnte. Man könnte damit direkt auch den Lieferant VIAC/CS Fonds ersetzen, welcher wegen der Übernahem der CreditSuisse durch die UBS allenfalls gar nicht mehr funktioniert.

Für den Fonds CH1336969105 ist die ID 175977 und der Endpunkt sieht entsprechend so aus:

https://sfd.lbswiss.ch/sfdpub/de/funds/excelData/175977

Die Response sieht dann so aus:

sep=;
VIAC Equity Emering Markets - CH1336969105 (CHF)
Date;CCY Chart Price;Chart Price;CCY Adjusted Chart Price;Adjusted Chart Price;CCY Total Return Chart Price;Total Return Chart Price;Net Asset Value;Issue Price;Redemption Price;Closing Price
2024-12-03;CHF;100;CHF;100;CHF;100;100;100.18;98.75;
2024-12-04;CHF;100;CHF;100;CHF;100;100;100;100;
2024-12-05;CHF;99.76;CHF;99.76;CHF;99.76;99.76;99.93;98.51;
2024-12-06;CHF;99.71;CHF;99.71;CHF;99.71;99.71;;;
2024-12-09;CHF;100.43;CHF;100.43;CHF;100.43;100.43;;;
2024-12-10;CHF;100.87;CHF;100.87;CHF;100.87;100.87;;;
2024-12-11;CHF;100.83;CHF;100.83;CHF;100.83;100.83;;;
2024-12-12;CHF;101.83;CHF;101.83;CHF;101.83;101.83;;;
2024-12-13;CHF;101.83;CHF;101.83;CHF;101.83;101.83;;;
2024-12-16;CHF;101.27;CHF;101.27;CHF;101.27;101.27;;;
2024-12-17;CHF;100.62;CHF;100.62;CHF;100.62;100.62;;;
2024-12-18;CHF;100.61;CHF;100.61;CHF;100.61;100.61;;;
2024-12-19;CHF;99.94;CHF;99.94;CHF;99.94;99.94;;;
2024-12-20;CHF;98.75;CHF;98.75;CHF;98.75;98.75;;;
2024-12-23;CHF;100.25;CHF;100.25;CHF;100.25;100.25;;;
2024-12-27;CHF;100.53;CHF;100.53;CHF;100.53;100.53;;;
2024-12-30;CHF;100.69;CHF;100.69;CHF;100.69;100.69;;;
2024-12-31;CHF;100.37;CHF;100.37;CHF;100.37;100.37;;;
2025-01-03;CHF;100.71;CHF;100.71;CHF;100.71;100.71;;;
2025-01-06;CHF;100.61;CHF;100.61;CHF;100.61;100.61;;;
2025-01-07;CHF;101.07;CHF;101.07;CHF;101.07;101.07;;;
2025-01-08;CHF;100.57;CHF;100.57;CHF;100.57;100.57;;;
2025-01-09;CHF;100.3;CHF;100.3;CHF;100.3;100.3;;;
2025-01-10;CHF;99.86;CHF;99.86;CHF;99.86;99.86;;;
2025-01-13;CHF;98.54;CHF;98.54;CHF;98.54;98.54;;;
2025-01-14;CHF;99.3;CHF;99.3;CHF;99.3;99.3;;;
2025-01-15;CHF;99.25;CHF;99.25;CHF;99.25;99.25;;;
2025-01-16;CHF;100.3;CHF;100.3;CHF;100.3;100.3;;;
2025-01-17;CHF;100.84;CHF;100.84;CHF;100.84;100.84;;;
2025-01-20;CHF;101.2;CHF;101.2;CHF;101.2;101.2;;;
2025-01-21;CHF;101.2;CHF;101.2;CHF;101.2;101.2;;;
2025-01-22;CHF;101.24;CHF;101.24;CHF;101.24;101.24;;;
2025-01-23;CHF;101.4;CHF;101.4;CHF;101.4;101.4;;;
2025-01-24;CHF;101.71;CHF;101.71;CHF;101.71;101.71;;;
2025-01-27;CHF;101.71;CHF;101.71;CHF;101.71;101.71;;;
2025-01-28;CHF;101.71;CHF;101.71;CHF;101.71;101.71;;;
2025-02-03;CHF;101.01;CHF;101.01;CHF;101.01;101.01;;;
2025-02-04;CHF;102.15;CHF;102.15;CHF;102.15;102.15;;;
2025-02-05;CHF;101.83;CHF;101.83;CHF;101.83;101.83;;;
2025-02-06;CHF;102.97;CHF;102.97;CHF;102.97;102.97;;;
2025-02-07;CHF;104.02;CHF;104.02;CHF;104.02;104.02;;;
2025-02-10;CHF;104.17;CHF;104.17;CHF;104.17;104.17;;;
2025-02-11;CHF;104.13;CHF;104.13;CHF;104.13;104.13;;;
2025-02-12;CHF;104.77;CHF;104.77;CHF;104.77;104.77;;;
2025-02-13;CHF;104.04;CHF;104.04;CHF;104.04;104.04;;;
2025-02-14;CHF;104.32;CHF;104.32;CHF;104.32;104.32;;;
2025-02-17;CHF;105.21;CHF;105.21;CHF;105.21;105.21;;;
2025-02-18;CHF;106.06;CHF;106.06;CHF;106.06;106.06;;;
2025-02-19;CHF;106.11;CHF;106.11;CHF;106.11;106.11;;;
2025-02-20;CHF;105.24;CHF;105.24;CHF;105.24;105.24;;;
2025-02-21;CHF;106.58;CHF;106.58;CHF;106.58;106.58;;;
2025-02-24;CHF;105.24;CHF;105.24;CHF;105.24;105.24;;;
2025-02-25;CHF;103.62;CHF;103.62;CHF;103.62;103.62;;;
2025-02-26;CHF;104.86;CHF;104.86;CHF;104.86;104.86;;;
2025-02-27;CHF;104.49;CHF;104.49;CHF;104.49;104.49;;;
2025-02-28;CHF;102.36;CHF;102.36;CHF;102.36;102.36;;;
2025-03-03;CHF;101.93;CHF;101.93;CHF;101.93;101.93;;;
2025-03-04;CHF;100.63;CHF;100.63;CHF;100.63;100.63;;;
2025-03-05;CHF;102.58;CHF;102.58;CHF;102.58;102.58;;;
2025-03-06;CHF;103.72;CHF;103.72;CHF;103.72;103.72;;;
2025-03-07;CHF;102.64;CHF;102.64;CHF;102.64;102.64;;;
2025-03-10;CHF;101.54;CHF;101.54;CHF;101.54;101.54;;;
2025-03-11;CHF;101.06;CHF;101.06;CHF;101.06;101.06;;;
2025-03-12;CHF;101.57;CHF;101.57;CHF;101.57;101.57;;;
2025-03-13;CHF;101.36;CHF;101.36;CHF;101.36;101.36;;;
2025-03-14;CHF;102.63;CHF;102.63;CHF;102.63;102.63;;;
2025-03-17;CHF;103.23;CHF;103.23;CHF;103.23;103.23;;;
2025-03-18;CHF;104.03;CHF;104.03;CHF;104.03;104.03;;;
2025-03-19;CHF;104.2;CHF;104.2;CHF;104.2;104.2;;;
2025-03-20;CHF;104.28;CHF;104.28;CHF;104.28;104.28;;;
2025-03-21;CHF;103.52;CHF;103.52;CHF;103.52;103.52;;;
2025-03-24;CHF;104.04;CHF;104.04;CHF;104.04;104.04;;;
2025-03-25;CHF;103.2;CHF;103.2;CHF;103.2;103.2;;;
2025-03-26;CHF;103.62;CHF;103.62;CHF;103.62;103.62;;;
2025-03-27;CHF;103.3;CHF;103.3;CHF;103.3;103.3;;;
2025-03-28;CHF;102.32;CHF;102.32;CHF;102.32;102.32;;;
2025-03-31;CHF;100.99;CHF;100.99;CHF;100.99;100.99;;;
2025-04-01;CHF;101.61;CHF;101.61;CHF;101.61;101.61;;;
2025-04-02;CHF;101.69;CHF;101.69;CHF;101.69;101.69;;;
2025-04-03;CHF;97.88;CHF;97.88;CHF;97.88;97.88;;;
2025-04-04;CHF;97.88;CHF;97.88;CHF;97.88;97.88;;;
2025-04-07;CHF;89.48;CHF;89.48;CHF;89.48;89.48;;;
2025-04-08;CHF;89.26;CHF;89.26;CHF;89.26;89.26;;;
2025-04-09;CHF;86.7;CHF;86.7;CHF;86.7;86.7;;;
2025-04-10;CHF;89.02;CHF;89.02;CHF;89.02;89.02;;;
2025-04-11;CHF;88.45;CHF;88.45;CHF;88.45;88.45;;;
2025-04-14;CHF;89.99;CHF;89.99;CHF;89.99;89.99;;;
2025-04-15;CHF;90.84;CHF;90.84;CHF;90.84;90.84;;;
2025-04-16;CHF;89.67;CHF;89.67;CHF;89.67;89.67;;;
2025-04-17;CHF;90.85;CHF;90.85;CHF;90.85;90.85;;;
2025-04-22;CHF;90.79;CHF;90.79;CHF;90.79;90.79;;;
2025-04-23;CHF;94.28;CHF;94.28;CHF;94.28;94.28;;;
2025-04-24;CHF;93.89;CHF;93.89;CHF;93.89;93.89;;;
2025-04-25;CHF;94.58;CHF;94.58;CHF;94.58;94.58;;;
2025-04-28;CHF;94.55;CHF;94.55;CHF;94.55;94.55;;;
2025-04-29;CHF;94.72;CHF;94.72;CHF;94.72;94.72;;;
2025-04-30;CHF;95.04;CHF;95.04;CHF;95.04;95.04;;;
2025-05-02;CHF;97.02;CHF;97.02;CHF;97.02;97.02;;;
2025-05-05;CHF;97.02;CHF;97.02;CHF;97.02;97.02;;;
2025-05-06;CHF;97.48;CHF;97.48;CHF;97.48;97.48;;;
2025-05-07;CHF;97.04;CHF;97.04;CHF;97.04;97.04;;;
2025-05-08;CHF;97.51;CHF;97.51;CHF;97.51;97.51;;;
2025-05-09;CHF;98.15;CHF;98.15;CHF;98.15;98.15;;;
2025-05-12;CHF;101.92;CHF;101.92;CHF;101.92;101.92;;;
2025-05-13;CHF;101.25;CHF;101.25;CHF;101.25;101.25;;;
2025-05-14;CHF;102.46;CHF;102.46;CHF;102.46;102.46;;;
2025-05-15;CHF;102.17;CHF;102.17;CHF;102.17;102.17;;;
2025-05-16;CHF;102.39;CHF;102.39;CHF;102.39;102.39;;;
2025-05-19;CHF;101.08;CHF;101.08;CHF;101.08;101.08;;;
2025-05-20;CHF;101.02;CHF;101.02;CHF;101.02;101.02;;;
2025-05-21;CHF;100.82;CHF;100.82;CHF;100.82;100.82;;;
2025-05-22;CHF;100.58;CHF;100.58;CHF;100.58;100.58;;;
2025-05-23;CHF;100.14;CHF;100.14;CHF;100.14;100.14;;;
2025-05-26;CHF;99.93;CHF;99.93;CHF;99.93;99.93;;;
2025-05-27;CHF;100.22;CHF;100.22;CHF;100.22;100.22;;;
2025-05-28;CHF;100.12;CHF;100.12;CHF;100.12;100.12;;;
2025-05-30;CHF;99.04;CHF;99.04;CHF;99.04;99.04;;;
2025-06-02;CHF;98.16;CHF;98.16;CHF;98.16;98.16;;;
2025-06-03;CHF;99.36;CHF;99.36;CHF;99.36;99.36;;;
2025-06-04;CHF;99.99;CHF;99.99;CHF;99.99;99.99;;;
2025-06-05;CHF;101.06;CHF;101.06;CHF;101.06;101.06;;;
2025-06-06;CHF;101.51;CHF;101.51;CHF;101.51;101.51;;;
2025-06-10;CHF;102.9;CHF;102.9;CHF;102.9;102.9;;;
2025-06-11;CHF;103.36;CHF;103.36;CHF;103.36;103.36;;;
2025-06-12;CHF;102.07;CHF;102.07;CHF;102.07;102.07;;;
2025-06-13;CHF;100.86;CHF;100.86;CHF;100.86;100.86;;;
2025-06-16;CHF;101.41;CHF;101.41;CHF;101.41;101.41;;;
2025-06-17;CHF;101.92;CHF;101.92;CHF;101.92;101.92;;;
2025-06-18;CHF;101.9;CHF;101.9;CHF;101.9;101.9;;;
2025-06-19;CHF;100.69;CHF;100.69;CHF;100.69;100.69;;;
2025-06-20;CHF;101.59;CHF;101.59;CHF;101.59;101.59;;;
2025-06-23;CHF;100.49;CHF;100.49;CHF;100.49;100.49;;;
2025-06-24;CHF;101.93;CHF;101.93;CHF;101.93;101.93;;;
2025-06-25;CHF;102.76;CHF;102.76;CHF;102.76;102.76;;;
2025-06-26;CHF;102.53;CHF;102.53;CHF;102.53;102.53;;;
2025-06-27;CHF;102.67;CHF;102.67;CHF;102.67;102.67;;;
2025-06-30;CHF;101.77;CHF;101.77;CHF;101.77;101.77;;;
2025-07-01;CHF;101.76;CHF;101.76;CHF;101.76;101.76;;;
2025-07-02;CHF;102.14;CHF;102.14;CHF;102.14;102.14;;;
2025-07-03;CHF;103.09;CHF;103.09;CHF;103.09;103.09;;;
2025-07-04;CHF;102.34;CHF;102.34;CHF;102.34;102.34;;;
2025-07-07;CHF;102.31;CHF;102.31;CHF;102.31;102.31;;;
2025-07-08;CHF;102.88;CHF;102.88;CHF;102.88;102.88;;;
2025-07-09;CHF;102.2;CHF;102.2;CHF;102.2;102.2;;;
2025-07-10;CHF;102.83;CHF;102.83;CHF;102.83;102.83;;;
2025-07-11;CHF;102.52;CHF;102.52;CHF;102.52;102.52;;;
2025-07-14;CHF;102.56;CHF;102.56;CHF;102.56;102.56;;;
2025-07-15;CHF;104.02;CHF;104.02;CHF;104.02;104.02;;;
2025-07-16;CHF;104.43;CHF;104.43;CHF;104.43;104.43;;;
2025-07-17;CHF;104.7;CHF;104.7;CHF;104.7;104.7;;;
2025-07-18;CHF;104.75;CHF;104.75;CHF;104.75;104.75;;;
2025-07-21;CHF;104.78;CHF;104.78;CHF;104.78;104.78;;;
2025-07-22;CHF;104.07;CHF;104.07;CHF;104.07;104.07;;;
2025-07-23;CHF;105.26;CHF;105.26;CHF;105.26;105.26;;;
2025-07-24;CHF;105.59;CHF;105.59;CHF;105.59;105.59;;;
2025-07-25;CHF;105.04;CHF;105.04;CHF;105.04;105.04;;;
2025-07-28;CHF;105.45;CHF;105.45;CHF;105.45;105.45;;;
2025-07-29;CHF;105.99;CHF;105.99;CHF;105.99;105.99;;;
2025-07-30;CHF;106.43;CHF;106.43;CHF;106.43;106.43;;;
2025-07-31;CHF;105.82;CHF;105.82;CHF;105.82;105.82;;;
2025-08-04;CHF;104.87;CHF;104.87;CHF;104.87;104.87;;;
2025-08-05;CHF;105.49;CHF;105.49;CHF;105.49;105.49;;;
2025-08-06;CHF;105.4;CHF;105.4;CHF;105.4;105.4;;;
2025-08-07;CHF;106.83;CHF;106.83;CHF;106.83;106.83;;;
2025-08-08;CHF;106.22;CHF;106.22;CHF;106.22;106.22;;;
2025-08-11;CHF;107.04;CHF;107.04;CHF;107.04;107.04;;;
2025-08-12;CHF;106.76;CHF;106.76;CHF;106.76;106.76;;;
2025-08-13;CHF;107.77;CHF;107.77;CHF;107.77;107.77;;;
2025-08-14;CHF;107.77;CHF;107.77;CHF;107.77;107.77;;;
2025-08-15;CHF;107.77;CHF;107.77;CHF;107.77;107.77;;;
2025-08-18;CHF;107.89;CHF;107.89;CHF;107.89;107.89;;;
2025-08-19;CHF;107.56;CHF;107.56;CHF;107.56;107.56;;;
2025-08-20;CHF;106.35;CHF;106.35;CHF;106.35;106.35;;;
2025-08-21;CHF;107.08;CHF;107.08;CHF;107.08;107.08;;;
2025-08-22;CHF;106.68;CHF;106.68;CHF;106.68;106.68;;;
2025-08-25;CHF;108.43;CHF;108.43;CHF;108.43;108.43;;;
2025-08-26;CHF;107.65;CHF;107.65;CHF;107.65;107.65;;;
2025-08-27;CHF;107.21;CHF;107.21;CHF;107.21;107.21;;;
2025-08-28;CHF;106.32;CHF;106.32;CHF;106.32;106.32;;;
2025-08-29;CHF;105.68;CHF;105.68;CHF;105.68;105.68;;;
2025-09-01;CHF;106.5;CHF;106.5;CHF;106.5;106.5;;;
2025-09-02;CHF;106.75;CHF;106.75;CHF;106.75;106.75;;;
2025-09-03;CHF;106.99;CHF;106.99;CHF;106.99;106.99;;;
2025-09-04;CHF;106.95;CHF;106.95;CHF;106.95;106.95;;;
2025-09-05;CHF;107.01;CHF;107.01;CHF;107.01;107.01;;;
2025-09-08;CHF;107;CHF;107;CHF;107;107;;;
2025-09-09;CHF;108.31;CHF;108.31;CHF;108.31;108.31;;;
2025-09-10;CHF;109.62;CHF;109.62;CHF;109.62;109.62;;;
2025-09-11;CHF;109.68;CHF;109.68;CHF;109.68;109.68;;;
2025-09-12;CHF;111.07;CHF;111.07;CHF;111.07;111.07;;;
2025-09-15;CHF;111.07;CHF;111.07;CHF;111.07;111.07;;;
2025-09-16;CHF;111.03;CHF;111.03;CHF;111.03;111.03;;;
2025-09-17;CHF;111.57;CHF;111.57;CHF;111.57;111.57;;;
2025-09-18;CHF;112.18;CHF;112.18;CHF;112.18;112.18;;;
2025-09-19;CHF;112.17;CHF;112.17;CHF;112.17;112.17;;;
2025-09-22;CHF;112.2;CHF;112.2;CHF;112.2;112.2;;;
2025-09-23;CHF;112.34;CHF;112.34;CHF;112.34;112.34;;;
2025-09-24;CHF;113.03;CHF;113.03;CHF;113.03;113.03;;;
2025-09-25;CHF;113.15;CHF;113.15;CHF;113.15;113.15;;;

Beste Grüsse
Jan

1 Like

Danke. Stimmt, könnte man wahrscheinlich automatisieren. Ich lade einfach einmal im Monat die Kurse so über die ID runter, formattiere die csv ein klein wenig und importiere diese dann bei PP.

Ja, bestimmt. Ich kann zwar nicht mit Eclipse umgehen und Java kann ich auch nicht, aber man kann ja mal mit AI-Hilfe ein wenig rumprobieren.

Ausgangspunkt: name.abuchen.portfolio/src/name/abuchen/portfolio/online/impl/CSQuoteFeed.java ist wahrscheinlich für das alte Credit Suisse Zeug, was jetzt nicht mehr geht.

Das habe ich (bzw. gemini) so verändert, das ein allgemeiner konfigurierbarer CSV-Provider dabei herauskommen soll. Funktioniert auch grundsätzlich


aber ich finde die Stelle nicht wo ich die Felder zum Konfigurieren (Datumsspalte, Kursspalte, Datumsformat, Trennzeichen usw.) in der? dem? UI zum Leben erwecke. In dem (geänderten) Code für CSQuoteFeed.java habe ich gemogelt

indem ich die Standardwerte einfach an lbswiss.ch angepasst habe. Aber so ist das ja eigentlich nicht gedacht, da sollen eigentlich Eingabefelder zu sehen sein, wie

im JSON-Provider. An welcher Stelle muss man da noch drehen?

Ich vermute irgendwo in name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/wizards/security/ und irgendwo in name.abuchen.portfolio.tests/src/name/abuchen/portfolio/online/impl/ aber ich weiß überhaupt nicht was genau man da tun sollte.

Die veränderte CSQuoteFeed.java

package name.abuchen.portfolio.online.impl;

import java.io.IOException;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import name.abuchen.portfolio.Messages;
import name.abuchen.portfolio.model.LatestSecurityPrice;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.SecurityProperty;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.online.QuoteFeed;
import name.abuchen.portfolio.online.QuoteFeedData;
import name.abuchen.portfolio.util.WebAccess;

/**
 * Ein flexibler Feed zur Verarbeitung von CSV-Dateien, dessen Konfiguration
 * über separate Security-Eigenschaften verwaltet wird.
 */
public class CSQuoteFeed implements QuoteFeed
{
    public static final String ID = "CONFIGURABLE_CSV_FEED"; //$NON-NLS-1$

    // Konfigurations-Konstanten, analog zum GenericJSONQuoteFeed
    public static final String SEPARATOR_PROPERTY_NAME = "CSV-SEPARATOR"; //$NON-NLS-1$
    public static final String DATE_COLUMN_PROPERTY_NAME = "CSV-DATE-COLUMN"; //$NON-NLS-1$
    public static final String PRICE_COLUMN_PROPERTY_NAME = "CSV-PRICE-COLUMN"; //$NON-NLS-1$
    public static final String DATE_FORMAT_PROPERTY_NAME = "CSV-DATE-FORMAT"; //$NON-NLS-1$
    public static final String SKIP_LINES_PROPERTY_NAME = "CSV-SKIP-LINES"; //$NON-NLS-1$

    // Standardwerte
    private static final String DEFAULT_SEPARATOR = ";"; //$NON-NLS-1$
    private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; //$NON-NLS-1$
    private static final int DEFAULT_DATE_COLUMN = 0;
    private static final int DEFAULT_PRICE_COLUMN = 7;
    private static final int DEFAULT_SKIP_LINES = 4;

    @Override
    public String getId()
    {
        return ID;
    }

    @Override
    public String getName()
    {
        return "Konfigurierbarer CSV Feed"; //$NON-NLS-1$
    }

    @Override
    public QuoteFeedData getHistoricalQuotes(Security security, boolean collectRawResponse)
    {
        // Da es keine Unterscheidung zwischen Latest/Historic wie beim
        // JSON-Feed gibt,
        // verwenden wir nur einen Satz von Konstanten.
        return internalGetQuotes(security, security.getFeedURL(), collectRawResponse);
    }

    private QuoteFeedData internalGetQuotes(Security security, String feedURL, boolean collectRawResponse)
    {
        try
        {
            if (feedURL == null || feedURL.length() == 0)
                throw new IOException(MessageFormat.format(Messages.MsgMissingFeedURL, security.getName()));

            // 1. Konfiguration aus den Security-Eigenschaften abrufen
            QuoteConfig config = getConfigFromSecurity(security);

            // 2. CSV-Inhalt herunterladen (hier wird nur die FeedURL verwendet,
            // keine Parameter)
            String content = new WebAccess(feedURL).get();
            QuoteFeedData result = new QuoteFeedData();

            if (collectRawResponse)
            {
                result.addResponse(feedURL, content);
            }

            // 3. Inhalt zeilenweise verarbeiten
            String[] lines = content.lines().toArray(String[]::new);

            // Schleife beginnt nach den zu überspringenden Zeilen
            for (int i = config.getSkipLines(); i < lines.length; i++)
            {
                String line = lines[i];
                if (line.trim().isEmpty())
                    continue;

                try
                {
                    result.addPrice(getPrice(line, config));
                }
                catch (Exception e)
                {
                    // Fehler beim Parsen einer Zeile protokollieren und
                    // fortfahren
                    System.err.println(MessageFormat.format("Fehler beim Parsen der Zeile für {0}: {1}. Fehler: {2}", //$NON-NLS-1$
                                    security.getName(), line, e.getMessage()));
                }
            }

            return result;
        }
        catch (Exception e)
        {
            return QuoteFeedData.withError(e);
        }
    }

    /**
     * Ruft die Konfigurationswerte aus den Security-Eigenschaften ab.
     */
    private QuoteConfig getConfigFromSecurity(Security security)
    {
        // Trennzeichen
        String separator = security.getPropertyValue(SecurityProperty.Type.FEED, SEPARATOR_PROPERTY_NAME)
                        .orElse(DEFAULT_SEPARATOR);

        // Datumsspalte
        int dateCol = security.getPropertyValue(SecurityProperty.Type.FEED, DATE_COLUMN_PROPERTY_NAME)
                        .map(Integer::parseInt).orElse(DEFAULT_DATE_COLUMN);

        // Kursspalte
        int priceCol = security.getPropertyValue(SecurityProperty.Type.FEED, PRICE_COLUMN_PROPERTY_NAME)
                        .map(Integer::parseInt).orElse(DEFAULT_PRICE_COLUMN);

        // Datumsformat
        String dateFormat = security.getPropertyValue(SecurityProperty.Type.FEED, DATE_FORMAT_PROPERTY_NAME)
                        .orElse(DEFAULT_DATE_FORMAT);

        // Zu überspringende Zeilen
        int skipLines = security.getPropertyValue(SecurityProperty.Type.FEED, SKIP_LINES_PROPERTY_NAME)
                        .map(Integer::parseInt).orElse(DEFAULT_SKIP_LINES);

        // Hier könnte man auch DATE_TIMEZONE und DATE_LOCALE übernehmen, falls
        // nötig
        // (analog zum JSON-Feed)

        return new QuoteConfig(separator, dateCol, priceCol, dateFormat, skipLines);
    }

    /**
     * Parsed eine einzelne Datenzeile basierend auf der Konfiguration.
     */
    private LatestSecurityPrice getPrice(String line, QuoteConfig config)
                    throws DateTimeParseException, NumberFormatException
    {
        // Entfernt führende 'sep=;' falls vorhanden (für das SFD-Beispiel
        // relevant)
        String cleanedLine = line.trim().replaceFirst("sep=;", ""); //$NON-NLS-1$//$NON-NLS-2$

        // Split mit dem konfigurierten Separator
        // Regex.quote schützt vor Sonderzeichen im Separator
        String regexSeparator = java.util.regex.Pattern.quote(config.getSeparator());
        String[] tokens = cleanedLine.split(regexSeparator);

        if (tokens.length <= config.getDateColumn() || tokens.length <= config.getPriceColumn())
        {
            throw new IllegalArgumentException("Nicht genügend Spalten in der Zeile."); //$NON-NLS-1$
        }

        // Datum parsen
        String dateString = tokens[config.getDateColumn()].trim();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(config.getDateFormat());
        LocalDate localDate = LocalDate.parse(dateString, formatter);

        // Preis parsen: Ersetzt Kommas durch Punkte (für deutsche/schweizer
        // Zahlenformate)
        String priceString = tokens[config.getPriceColumn()].trim().replace(',', '.');
        double rawPrice = Double.parseDouble(priceString);

        // Faktorisiert den Preis
        long price = Values.Quote.factorize(rawPrice);

        return new LatestSecurityPrice(localDate, price);
    }
}

/**
 * Hilfsklasse zur Speicherung der Konfigurationsparameter. Die baseURL
 * entfällt, da sie die normale FeedURL ist.
 */
class QuoteConfig
{
    private final String separator;
    private final int dateColumn;
    private final int priceColumn;
    private final String dateFormat;
    private final int skipLines;

    public QuoteConfig(String separator, int dateColumn, int priceColumn, String dateFormat, int skipLines)
    {
        this.separator = separator;
        this.dateColumn = dateColumn;
        this.priceColumn = priceColumn;
        this.dateFormat = dateFormat;
        this.skipLines = skipLines;
    }

    public String getSeparator()
    {
        return separator;
    }

    public int getDateColumn()
    {
        return dateColumn;
    }

    public int getPriceColumn()
    {
        return priceColumn;
    }

    public int getSkipLines()
    {
        return skipLines;
    }

    public String getDateFormat()
    {
        return dateFormat;
    }
}

Ich habe das Problem anders gelöst. Ich habe mir ein kleines Python Script geschrieben und hole damit über GitHub Actions jeden Abend die CSV, konvertiere nach JSON und schiebe das JSON in einen anderen Branch wieder hoch. Den Branch deploye ich per Cloudflare Pages (da GH Pages nicht mehr gratis ist) auf meine eigene Domain und binde den Link bei PP als normaler JSON import ein. Funktioniert hervorragend soweit. Eleganter oder leichter zu teilen wäre natürlich die native Integration in PP von SwissFundData.

Mein GH Repo ist nicht öffentlich aber die JSON ist frei abrufbar. Ich möchte den Link hier nicht öffentlich machen aber auf Anfrage kann ich den Link teilen. Die JSON aktualisiert einmal täglich aktuell.

Klingt nach einer schönen Lösung nbgit10, kannst du das Python Script dafür öffentlich machen?

Ich hab’s einfach ge-vibecoded. Ist also ein “einfaches” Python Skript. In einer JSON hinterlege ich ISIN, Name und die ID von Swissfundsdata.

Python-Skript
"""
VIAC Fund Data Generator

Downloads CSV files from SwissFundData for VIAC funds and converts them to JSON format
for use with Portfolio Performance software.
"""

import io
import json
import os
import pathlib
import sys

import pandas as pd
import requests

BASE_URL = "https://www.swissfunddata.ch/sfdpub/de/funds/excelData/{id}"
TOKEN_ENV = "VIAC_JSON_TOKEN"
DEFAULT_TOKEN = "example-token"
OUTPUT_ROOT = pathlib.Path("json")
FUNDS_FILE = pathlib.Path("funds.json")


def load_funds() -> list[dict]:
    """Load fund definitions from funds.json file."""
    if not FUNDS_FILE.exists():
        raise FileNotFoundError(f"Missing {FUNDS_FILE}")
    with FUNDS_FILE.open(encoding="utf-8") as f:
        data = json.load(f)
    if not isinstance(data, list):
        raise ValueError("funds.json must contain a list of fund definitions")
    required = {"id", "isin", "name", "currency"}
    for fund in data:
        if not required.issubset(fund):
            raise ValueError(f"Fund entry missing keys {required - set(fund)}: {fund}")
    return data


def get_token() -> str:
    """Get the secret token from environment variable or use default."""
    token = os.environ.get(TOKEN_ENV, DEFAULT_TOKEN).strip()
    if token == DEFAULT_TOKEN:
        print(
            f"⚠️  Using default token '{DEFAULT_TOKEN}'. "
            f"Set {TOKEN_ENV} to a secret string for production.",
            file=sys.stderr,
        )
    if not token:
        raise ValueError(f"Empty token. Set {TOKEN_ENV}.")
    return token


def fetch_csv(fund_id: str) -> str:
    """Download CSV file from SwissFundData for given fund ID."""
    url = BASE_URL.format(id=fund_id)
    resp = requests.get(url, timeout=60)
    resp.raise_for_status()
    return resp.text


def parse_csv(csv_text: str) -> pd.DataFrame:
    """
    Parse SwissFundData CSV format to dataframe.

    CSV format:
    - Line 1: sep=;
    - Line 2: Fund name and ISIN
    - Line 3: Headers
    - Line 4+: Data rows
    """
    # Read CSV with semicolon separator, skipping first line (sep=;) and second line (title)
    df = pd.read_csv(
        io.StringIO(csv_text),
        sep=";",
        skiprows=2,  # Skip "sep=;" and title line
    )

    # Extract Date and Net Asset Value columns
    if "Date" not in df.columns:
        raise ValueError("CSV missing 'Date' column")

    # Use Net Asset Value as the price (most reliable column)
    price_col = "Net Asset Value"
    if price_col not in df.columns:
        raise ValueError(f"CSV missing '{price_col}' column")

    # Select and rename columns
    df = df[["Date", price_col]].rename(columns={"Date": "date", price_col: "close"})

    # Clean and normalize
    df = df.dropna(subset=["date", "close"])

    # Parse dates and format as ISO strings
    df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce")
    df = df.dropna(subset=["date"])  # Remove rows with invalid dates
    df["date"] = df["date"].dt.strftime("%Y-%m-%d")

    # Parse prices
    df["close"] = pd.to_numeric(df["close"], errors="coerce")
    df = df.dropna(subset=["close"])

    # Sort by date
    df = df.sort_values("date")

    return df


def save_json(df: pd.DataFrame, out_path: pathlib.Path) -> None:
    """Save dataframe as JSON array with date and close price objects."""
    out_path.parent.mkdir(parents=True, exist_ok=True)
    records = [{"date": str(row.date), "close": float(row.close)} for row in df.itertuples()]
    with out_path.open("w", encoding="utf-8") as f:
        json.dump(records, f, ensure_ascii=False, indent=2)


def process_fund(fund: dict, token: str) -> None:
    """Process a single fund: download CSV, convert to JSON, and save."""
    fund_id = fund["id"]
    isin = fund["isin"]
    out_path = OUTPUT_ROOT / token / f"{isin}.json"
    print(f"Processing {fund_id} - {fund['name']} ({isin}) -> {out_path}")
    csv_text = fetch_csv(fund_id)
    df = parse_csv(csv_text)
    save_json(df, out_path)
    print(f"  {len(df)} rows written")


def main() -> int:
    """Main entry point: process all funds from funds.json."""
    funds = load_funds()
    token = get_token()
    if not funds:
        print("funds.json is empty; nothing to do.")
        return 0
    for fund in funds:
        try:
            process_fund(fund, token)
        except Exception as exc:  # noqa: BLE001
            print(f"Failed for {fund['id']} ({fund['name']}): {exc}", file=sys.stderr)
    return 0


if __name__ == "__main__":
    sys.exit(main())
1 Like