VIAC Invest in Portfolio Performance einbinden

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;
    }
}