diff --git a/Aktienkurse.md b/Aktienkurse.md index 5c07656..a2967c9 100644 --- a/Aktienkurse.md +++ b/Aktienkurse.md @@ -14,6 +14,11 @@ Für den kostenlosen, tagesaktuellen Abruf von Aktienkursen mit ISIN-Codes (ca. ### 2. **Financial Modeling Prep (FMP)** - **Angebot:** 250 kostenlose Anfragen pro Tag (mehr als ausreichend für 20 Positionen). - **ISIN-Unterstützung:** Ja, ISIN kann für die Suche verwendet werden. +- **Stand August 2026 – Einschränkungen im kostenlosen Tarif:** Die alten `/api/v3/`-Adressen + sind seit dem 31.08.2025 abgeschaltet (HTTP 403), gültig ist nur noch `/stable/`. Die + ISIN-Suche (`/stable/search-isin`) und deutsche Börsenplätze (`EUNL.DE` und dergleichen) + sind kostenpflichtig und antworten mit HTTP 402. Für ein Depot mit deutschen ISINs bleibt + im Gratistarif also nichts übrig – dafür Leeway verwenden. - **Daten:** Echtzeit- und historische Kurse, Fundamentaldaten, Dividenden. - **Registrierung:** Kostenlos, API-Key nach Anmeldung. - **Dokumentation:** [fmpcloud.io](https://site.financialmodelingprep.com/de/developer/docs) @@ -27,11 +32,34 @@ Für den kostenlosen, tagesaktuellen Abruf von Aktienkursen mit ISIN-Codes (ca. - **Dokumentation:** [www.alphavantage.co](https://www.alphavantage.co) --- -### 4. **Yahoo Finance (unoffiziell)** -- **Angebot:** Kein offizieller API-Key nötig, aber Nutzung der öffentlichen URL-Struktur (z. B. `https://query1.finance.yahoo.com/v8/finance/chart/ISIN?interval=1d`). -- **ISIN-Unterstützung:** Teilweise, aber nicht alle ISINs werden direkt unterstützt. Oft muss die ISIN auf ein Börsenkürzel gemappt werden. -- **Daten:** Aktuelle Kurse, aber mit Verzögerung und ohne Garantie auf Stabilität. -- **Hinweis:** Keine offizielle API, daher kann die Nutzung eingeschränkt oder blockiert werden . +### 4. **Yahoo Finance (unoffiziell)** – im Finanzmanager umgesetzt +- **Angebot:** Kein API-Key, kein Kontingent. +- **ISIN-Unterstützung:** Keine mehr. `/lookup` und `/search` antworten mit HTTP 404, die + Suchschnittstelle `v1/finance/search` mit HTTP 429. Das Börsenkürzel muss deshalb in den + Stammdaten des Wertpapiers stehen. +- **Stand August 2026:** Auch `query1.finance.yahoo.com` (chart, quote, getcrumb) antwortet + ohne Sitzung durchgängig mit HTTP 429 – Cookie und Crumb helfen nicht. Erreichbar ist nur + die Kursseite `https://de.finance.yahoo.com/quote/ALV.DE/`, aus der der Kurs gelesen wird. +- **Fallstricke beim Lesen der Seite:** + - Die Seite wird in zwei Ausprägungen ausgeliefert – mit `` + (Punkt als Dezimaltrenner) oder nur mit dem angezeigten Kurs (`data-testid="qsp-price"`, + deutsche Schreibweise). Beides kommt am selben Tag vor. + - Das eingebettete JSON (`"regularMarketPrice":{"raw":…}`) gehört zu einem Fremd-Widget + und führt in die Irre: Es liefert auf jeder Seite denselben, papierfremden Wert. + - Die Kopfleiste führt DAX, Dow und andere als `` mit – nur der Treffer mit + passendem `data-symbol` zählt. + - Ohne `Accept: text/html` gibt das Auslieferungsnetz gelegentlich eine veraltete Fassung + heraus. Der angezeigte Kursstand landet im Debug-Log. + - Schnelle Serienabrufe beantwortet Yahoo mit HTTP 503, deshalb eine Sekunde Abstand. + Der 503 hält auch mal eine Viertelstunde an und trifft dann einzelne Kürzel, die kurz + darauf wieder ausgeliefert werden – im Browser funktionieren sie zur selben Zeit. Der + Abruf wiederholt deshalb bis zu dreimal. + - Ein **Browser-User-Agent schadet**: Mit Chrome-Kennung antwortete Yahoo mit HTTP 404, + mit `Finanzmanager Kursabruf` mit 200. Getarnt wird hier nichts. +- **Hinweis:** Keine offizielle Schnittstelle. Die Nutzungsbedingungen von Yahoo sehen + automatisierte Abrufe nicht vor; für ein privates Depot mit einer Handvoll Papieren am + Tag ist das vertretbar, als Grundlage für mehr aber nicht geeignet. Die `robots.txt` + sperrt `/quote/` nicht. --- ### 5. **Marketstack** diff --git a/Changes.md b/Changes.md index 74aa6cf..2e23dc7 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,44 @@ # Änderungen +## Kurse von Yahoo Finanzen (13.08.2026) + +Dritter Anbieter im Kursabruf: **Yahoo Finanzen**, ohne Schlüssel und ohne Tageskontingent. +Eine offene Schnittstelle gibt es dort nicht mehr – `query1.finance.yahoo.com` antwortet +ohne Sitzung mit HTTP 429, auch mit Cookie und Crumb –, gelesen wird deshalb die Kursseite +selbst. + +- Auswahl *Yahoo Finanzen* im Einstellungsdialog unter *Wertpapiere*; das Feld für den + API-Schlüssel bleibt dort leer. +- Das **Kürzel muss in den Stammdaten stehen** und den Handelsplatz nennen (`ALV.DE`, + `PTX.F`). Yahoo hat keine ISIN-Suche mehr, `/lookup` und `/search` antworten mit 404. +- `Kursabruf::yahooKurs()` liest beide Ausprägungen der Seite (`` und den + angezeigten Kurs in deutscher Schreibweise) und prüft am Seitentitel, dass die Seite zum + Kürzel gehört – ein Tippfehler bringt so keinen fremden Kurs in den Bestand. +- Eine Sekunde Abstand zwischen zwei Seiten; auf schnelle Serienabrufe antwortet Yahoo mit + HTTP 503. Weil dieser 503 vorübergehend ist und Kürzel trifft, die im Browser zur selben + Zeit funktionieren, wird bis zu dreimal angefragt. Der angezeigte Kursstand geht ins + Debug-Log, weil Yahoo gelegentlich eine ältere Fassung der Seite ausliefert. + +Der Weg über fremdes Markup ist der empfindlichste der drei Anbieter: Baut Yahoo die Seite +um, meldet der Abruf einen Fehler, statt einen falschen Kurs zu speichern. + +## Kursabruf: Financial Modeling Prep antwortete mit HTTP 403 (13.08.2026) + +Financial Modeling Prep hat die alten `/api/v3/`-Adressen zum 31. August 2025 abgeschaltet; +sie beantworten seither jede Anfrage mit HTTP 403. Der Kursabruf sprach genau diese Adressen +an und meldete daraufhin *„Der API-Schlüssel wurde abgelehnt“* – eine falsche Fährte, denn +der Schlüssel war in Ordnung. + +- `Kursabruf` nutzt für Financial Modeling Prep jetzt `/stable/quote` und `/stable/search-isin`. +- Fehlermeldungen nennen den Klartext des Dienstes. HTTP 403 heißt nicht mehr pauschal + „Schlüssel abgelehnt“, und HTTP 402 („im gebuchten Tarif nicht enthalten“) wird als + eigener Fall gemeldet statt als nacktes „antwortet mit HTTP 402“. + +**Hinweis zum kostenlosen Tarif von Financial Modeling Prep:** Er umfasst weder die +ISIN-Suche noch deutsche Börsenplätze (`…​.DE`) – beides beantwortet der Dienst mit HTTP 402. +Für ein Depot mit deutschen ISINs ist dort auch nach dieser Korrektur nichts zu holen; +dafür ist Leeway.tech vorgesehen (Einstellungen → Wertpapiere). + ## Detailseite eines Wertpapiers (13.08.2026) Bisher endete der Weg zu einem Wertpapier in einer Tabellenzeile: Stückzahl, Kurs, Wert – diff --git a/docs/wertpapiere.md b/docs/wertpapiere.md index 0cfaf2a..7e1ca45 100644 --- a/docs/wertpapiere.md +++ b/docs/wertpapiere.md @@ -151,15 +151,37 @@ Ersatzwert wäre bequem, verstellte aber den Blick. ### Kursabruf -`Services\Kursabruf` kennt zwei Anbieter (siehe `Aktienkurse.md`): +`Services\Kursabruf` kennt drei Anbieter (siehe `Aktienkurse.md`): * **Leeway.tech** – 50 Abrufe/Tag, kennt ISINs direkt -* **Financial Modeling Prep** – 250 Abrufe/Tag +* **Financial Modeling Prep** – 250 Abrufe/Tag, im Gratistarif ohne ISIN-Suche und ohne + deutsche Börsenplätze (beides antwortet mit HTTP 402) +* **Yahoo Finanzen** – kein Schlüssel, kein Kontingent, dafür muss das Kürzel in den + Stammdaten stehen -Beide arbeiten in zwei Schritten: ISIN → Kürzel, Kürzel → Kurs. Das gefundene Kürzel wird -am Wertpapier gespeichert, damit der erste Schritt künftig entfällt – das halbiert den -Verbrauch. Abgerufen wird nur für Papiere **mit Bestand**, und nur, wenn für heute noch -kein Kurs vorliegt. +Die ersten beiden arbeiten in zwei Schritten: ISIN → Kürzel, Kürzel → Kurs. Das gefundene +Kürzel wird am Wertpapier gespeichert, damit der erste Schritt künftig entfällt – das +halbiert den Verbrauch. Abgerufen wird nur für Papiere **mit Bestand**, und nur, wenn für +heute noch kein Kurs vorliegt. + +**Yahoo Finanzen** hat keine offene Schnittstelle mehr: `query1.finance.yahoo.com` +antwortet ohne Sitzung mit HTTP 429, auch mit Cookie und Crumb, und `/lookup` wie `/search` +sind abgeschaltet. Gelesen wird deshalb die Kursseite `de.finance.yahoo.com/quote//`. + +* Das **Kürzel** muss den Handelsplatz nennen – `ALV.DE` für Xetra, `PTX.F` für Frankfurt. + Ohne Kürzel meldet der Abruf das ausdrücklich, denn raten lässt sich hier nichts. +* Yahoo liefert die Seite in zwei Ausprägungen aus: mal mit `` samt + `data-value`, mal nur mit dem angezeigten, deutsch formatierten Kurs. `yahooKurs()` liest + beides und prüft am Seitentitel, dass die Seite zum Kürzel gehört – sonst käme bei einem + Tippfehler der Kurs eines fremden Papiers in den Bestand. +* Zwischen zwei Seiten liegt **eine Sekunde**; auf schnelle Serienabrufe antwortet Yahoo + mit HTTP 503. Dieser 503 ist vorübergehend – er trifft auch Kürzel, die im Browser zur + selben Zeit funktionieren –, deshalb bis zu drei Versuche je Papier. +* Der angezeigte Kursstand („Markt geöffnet“, „Bei Börsenschluss“) geht ins Debug-Log: + Über sein Auslieferungsnetz gibt Yahoo gelegentlich eine ältere Fassung der Seite heraus. + +Weil hier fremdes Markup gelesen wird, ist dieser Weg der empfindlichste der drei – ein +Umbau bei Yahoo fällt als Fehler auf, nicht als falscher Kurs. Anbieter und Schlüssel stehen im Einstellungsdialog unter *Wertpapiere*. Der tägliche Lauf gehört in einen Cron-Eintrag: diff --git a/src/Services/Kursabruf.php b/src/Services/Kursabruf.php index 4c55ef0..5dce4c9 100644 --- a/src/Services/Kursabruf.php +++ b/src/Services/Kursabruf.php @@ -10,9 +10,10 @@ use Finanzmanager\Models\Wertpapier; /** * Holt Tageskurse von einem Kursdienst und legt sie in `kurse` ab. * - * Zwei Anbieter, beide mit kostenlosem Kontingent (siehe `Aktienkurse.md`): - * **Leeway.tech** (50 Abrufe/Tag, kennt ISINs direkt) und **Financial Modeling - * Prep** (250/Tag). Bei rund zwanzig Positionen reicht beides. + * Drei Anbieter (siehe `Aktienkurse.md`): **Leeway.tech** (50 Abrufe/Tag, kennt + * ISINs direkt), **Financial Modeling Prep** (250/Tag, im Gratistarif ohne + * ISIN-Suche und ohne deutsche Börsenplätze) und **Yahoo Finanzen**, das ohne + * Schlüssel auskommt, dafür aber das Kürzel in den Stammdaten braucht. * * Der Anbieter ist austauschbar: Jeder liefert zu einem Wertpapier einen Kurs * oder null, alles andere – Reihenfolge, Ablage, Protokoll – ist gemeinsam. @@ -26,11 +27,30 @@ class Kursabruf public const ANBIETER = [ 'leeway' => 'Leeway.tech (50 Abrufe/Tag)', 'fmp' => 'Financial Modeling Prep (250 Abrufe/Tag)', + 'yahoo' => 'Yahoo Finanzen (ohne Schlüssel, Kürzel nötig)', ]; + /** Anbieter, die ohne API-Schlüssel auskommen. */ + private const OHNE_SCHLUESSEL = ['yahoo']; + /** Wie lange auf den Kursdienst gewartet wird, in Sekunden. */ private const ZEITGRENZE = 15; + /** Mindestabstand zwischen zwei Kursseiten, in Sekunden. */ + private const SEITENPAUSE = 1.0; + + /** Wie oft eine Kursseite geholt wird, wenn der Anbieter kurz streikt. */ + private const SEITENVERSUCHE = 3; + + /** Wartezeit vor dem nächsten Versuch, in Sekunden. */ + private const WIEDERHOLPAUSE = 2; + + /** Zeitpunkt der zuletzt geholten Kursseite. */ + private static float $letzteSeite = 0.0; + + /** Statuscode der letzten Antwort – für die Frage, ob ein zweiter Versuch lohnt. */ + private static int $letzterStatus = 0; + /** * Holt Kurse für alle Wertpapiere mit Bestand. * @@ -50,7 +70,7 @@ class Kursabruf if (!array_key_exists($anbieter, self::ANBIETER)) { return self::ergebnis(0, 1, 0, ['Unbekannter Kursdienst: ' . $anbieter]); } - if ($key === '') { + if ($key === '' && !in_array($anbieter, self::OHNE_SCHLUESSEL, true)) { return self::ergebnis(0, 1, 0, [ 'Kein API-Schlüssel hinterlegt (Einstellungen → Wertpapiere).', ]); @@ -127,6 +147,7 @@ class Kursabruf { return match ($anbieter) { 'fmp' => self::fmp($key, $wertpapier), + 'yahoo' => self::yahoo($wertpapier), default => self::leeway($key, $wertpapier), }; } @@ -174,6 +195,9 @@ class Kursabruf * Financial Modeling Prep sucht die ISIN erst zu einem Kürzel, dann kommt * der Kurs über die Kurzabfrage. * + * Seit dem 31. August 2025 beantwortet der Dienst die alten `/api/v3/`- + * Adressen nur noch mit HTTP 403; gültig ist allein `/stable/`. + * * @param array $wertpapier */ private static function fmp(string $key, array $wertpapier): ?float @@ -187,8 +211,8 @@ class Kursabruf } $treffer = self::hole( - 'https://financialmodelingprep.com/api/v3/search?query=' . rawurlencode($isin) - . '&limit=1&apikey=' . rawurlencode($key) + 'https://financialmodelingprep.com/stable/search-isin?isin=' . rawurlencode($isin) + . '&apikey=' . rawurlencode($key) ); $symbol = (string)self::ersterWert($treffer[0] ?? [], ['symbol', 'Symbol']); @@ -200,19 +224,160 @@ class Kursabruf } $antwort = self::hole( - 'https://financialmodelingprep.com/api/v3/quote-short/' . rawurlencode($symbol) - . '?apikey=' . rawurlencode($key) + 'https://financialmodelingprep.com/stable/quote?symbol=' . rawurlencode($symbol) + . '&apikey=' . rawurlencode($key) ); return self::zahl(self::ersterWert($antwort[0] ?? $antwort, ['price', 'Price'])); } + /** + * Yahoo Finanzen hat keine offene Schnittstelle mehr: `query1.finance.yahoo.com` + * antwortet auf Anfragen ohne Sitzung mit HTTP 429, auch mit Cookie und Crumb. + * Übrig bleibt die Kursseite selbst, aus der der Kurs gelesen wird. + * + * Der Preis dafür ist Abhängigkeit von fremdem Markup – deshalb zwei Muster + * und ein Abgleich mit dem Seitentitel, damit ein Umbau bei Yahoo als Fehler + * auffällt und nicht als falscher Kurs in den Bestand wandert. + * + * Eine ISIN-Suche gibt es dort nicht mehr (`/lookup` und `/search` antworten + * mit 404), das Kürzel muss deshalb in den Stammdaten stehen. + * + * @param array $wertpapier + */ + private static function yahoo(array $wertpapier): ?float + { + $symbol = trim((string)$wertpapier['symbol']); + + if ($symbol === '') { + throw new \RuntimeException( + 'Yahoo Finanzen kennt keine ISIN-Suche – bitte das Kürzel in den ' + . 'Stammdaten hinterlegen (Xetra-Werte enden auf .DE, etwa ALV.DE).' + ); + } + + $html = self::holeSeite('https://de.finance.yahoo.com/quote/' . rawurlencode($symbol) . '/'); + + // Yahoo liefert über sein Auslieferungsnetz gelegentlich eine ältere + // Fassung der Seite. Der angezeigte Stand („Markt geöffnet“, „Bei + // Börsenschluss“) landet deshalb im Protokoll – daran lässt sich ein + // alter Kurs erkennen, wenn einer auffällt. + if (preg_match('/marketTimeNotice[^>]*>(.*?)<\/div>/s', $html, $stand) === 1) { + DebugLog::schreibe( + 'kursabruf', + $symbol . ': ' . trim((string)preg_replace('/\s+/', ' ', strip_tags($stand[1]))) + ); + } + + return self::yahooKurs($html, $symbol); + } + + /** + * Der Kurs aus einer Yahoo-Kursseite. Getrennt vom Abruf, damit die + * Auswertung ohne Netz geprüft werden kann. + * + * Yahoo liefert die Seite in zwei Ausprägungen aus – mal mit dem + * maschinenlesbaren `` (Punkt als Dezimaltrenner), mal nur mit + * dem angezeigten Kurs (deutsche Schreibweise). Beides kommt vor, teils am + * selben Tag für verschiedene Papiere. + */ + public static function yahooKurs(string $html, string $symbol): ?float + { + // Der Titel lautet „Allianz SE (ALV.DE) Aktienkurs …“. Steht dort ein + // anderes Kürzel, zeigt die Seite ein anderes Papier – dann lieber kein + // Kurs als der falsche. + if (preg_match('#(.*?)#is', $html, $titel) === 1 + && !str_contains($titel[1], '(' . $symbol . ')') + ) { + throw new \RuntimeException( + 'Yahoo zeigt unter dem Kürzel ' . $symbol . ' eine andere Seite an.' + ); + } + + $streamer = '/]*data-symbol="' . preg_quote($symbol, '/') . '"' + . '[^>]*data-field="regularMarketPrice"[^>]*data-value="([0-9.]+)"/i'; + + if (preg_match($streamer, $html, $treffer) === 1) { + return self::zahl($treffer[1]); + } + + if (preg_match('/data-testid="qsp-price"[^>]*>([^<]+) */ private static function hole(string $url): array + { + $antwort = self::abruf($url, 'application/json', false); + + $daten = json_decode($antwort, true); + if (!is_array($daten)) { + throw new \RuntimeException('Antwort des Kursdienstes ist kein JSON.'); + } + + return $daten; + } + + /** + * Eine Kursseite als HTML. Yahoo leitet von `/quote/X/` auf die Länderseite + * weiter und liefert komprimiert – beides muss curl mitmachen. + * + * Zwischen zwei Seiten liegt eine Sekunde: Auf schnelle Serienabrufe + * antwortet Yahoo mit HTTP 503. Bei einer Handvoll Papiere am Tag fällt die + * Wartezeit nicht ins Gewicht. + * + * Ein 503 ist bei Yahoo vorübergehend und trifft auch Kürzel, die kurz + * darauf wieder ausgeliefert werden – deshalb bis zu drei Versuche, statt + * das Papier für den Tag verloren zu geben. + */ + private static function holeSeite(string $url): string + { + for ($versuch = 1; ; $versuch++) { + $wartezeit = self::SEITENPAUSE - (microtime(true) - self::$letzteSeite); + if (self::$letzteSeite > 0.0 && $wartezeit > 0.0) { + usleep((int)($wartezeit * 1_000_000)); + } + self::$letzteSeite = microtime(true); + + try { + return self::abruf($url, 'text/html', true); + } catch (\RuntimeException $fehler) { + if (self::$letzterStatus !== 503 || $versuch >= self::SEITENVERSUCHE) { + throw $fehler; + } + + sleep(self::WIEDERHOLPAUSE); + } + } + } + + /** + * Ein GET gegen den Kursdienst, Rohtext der Antwort. + */ + private static function abruf(string $url, string $accept, bool $folgen): string { $kanal = curl_init($url); if ($kanal === false) { @@ -223,9 +388,11 @@ class Kursabruf CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => self::ZEITGRENZE, CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_FOLLOWLOCATION => false, + CURLOPT_FOLLOWLOCATION => $folgen, + CURLOPT_MAXREDIRS => 3, + CURLOPT_ENCODING => '', CURLOPT_USERAGENT => 'Finanzmanager Kursabruf', - CURLOPT_HTTPHEADER => ['Accept: application/json'], + CURLOPT_HTTPHEADER => ['Accept: ' . $accept], ]); $antwort = curl_exec($kanal); @@ -233,6 +400,8 @@ class Kursabruf $fehler = curl_error($kanal); curl_close($kanal); + self::$letzterStatus = $status; + // Der Schlüssel steht in der Adresse – er darf nirgends im Klartext // auftauchen, auch nicht in einer Fehlermeldung. $sichtbar = preg_replace('/(apitoken|apikey)=[^&]*/', '$1=…', $url) ?? $url; @@ -241,22 +410,30 @@ class Kursabruf if ($antwort === false) { throw new \RuntimeException('Kursdienst nicht erreichbar: ' . $fehler); } - if ($status === 401 || $status === 403) { - throw new \RuntimeException('Der API-Schlüssel wurde abgelehnt (HTTP ' . $status . ').'); + + // Der Klartext des Dienstes wird mitgegeben: Ein 403 heißt bei Financial + // Modeling Prep längst nicht mehr nur „Schlüssel falsch“, sondern meist + // „diese Adresse gibt es nicht mehr“ – ohne den Text sucht man am + // falschen Ende. + $grund = self::meldung((string)$antwort); + + if ($status === 401) { + throw new \RuntimeException('Der API-Schlüssel wurde abgelehnt (HTTP 401).' . $grund); + } + if ($status === 402) { + throw new \RuntimeException('Im gebuchten Tarif nicht enthalten (HTTP 402).' . $grund); + } + if ($status === 403) { + throw new \RuntimeException('Zugriff verweigert (HTTP 403).' . $grund); } if ($status === 429) { throw new \RuntimeException('Das Tageskontingent des Kursdienstes ist aufgebraucht.'); } if ($status < 200 || $status >= 300) { - throw new \RuntimeException('Kursdienst antwortet mit HTTP ' . $status . '.'); + throw new \RuntimeException('Kursdienst antwortet mit HTTP ' . $status . '.' . $grund); } - $daten = json_decode((string)$antwort, true); - if (!is_array($daten)) { - throw new \RuntimeException('Antwort des Kursdienstes ist kein JSON.'); - } - - return $daten; + return (string)$antwort; } /** @@ -280,6 +457,35 @@ class Kursabruf return null; } + /** + * Die Begründung des Dienstes, gekürzt und einzeilig – als Anhang an die + * eigene Fehlermeldung. Die Anbieter antworten mal mit JSON, mal mit + * schlichtem Text, deshalb beides. + */ + private static function meldung(string $antwort): string + { + // Fehlerseiten der Kursseiten-Anbieter sind ganze HTML-Dokumente – davon + // gehört nichts in eine Fehlermeldung. + if (str_starts_with(ltrim($antwort), '<')) { + return ''; + } + + $daten = json_decode($antwort, true); + $text = is_array($daten) + ? (string)(self::ersterWert($daten, ['Error Message', 'error', 'message', 'Message']) ?? '') + : $antwort; + + $text = trim(preg_replace('/\s+/', ' ', $text) ?? ''); + if ($text === '') { + return ''; + } + if (mb_strlen($text) > 200) { + $text = mb_substr($text, 0, 200) . '…'; + } + + return ' ' . $text; + } + private static function zahl(mixed $wert): ?float { if ($wert === null || !is_numeric($wert)) { diff --git a/src/Views/einstellungen/index.php b/src/Views/einstellungen/index.php index bb0f08d..2b6be9d 100644 --- a/src/Views/einstellungen/index.php +++ b/src/Views/einstellungen/index.php @@ -157,16 +157,19 @@ include __DIR__ . '/../layout/kopf.php';

- Beide kennen ISINs und haben ein kostenloses Tageskontingent, das für rund zwanzig - Positionen reicht. + Leeway kennt ISINs und hat ein kostenloses Tageskontingent, das für rund zwanzig + Positionen reicht. Financial Modeling Prep führt ISIN-Suche und deutsche + Börsenplätze nur im Bezahltarif. Yahoo Finanzen braucht keinen Schlüssel, dafür + muss bei jedem Wertpapier das Kürzel samt Handelsplatz stehen (etwa + ALV.DE für Xetra).

- Nach kostenloser Anmeldung beim gewählten Dienst. Ohne Schlüssel lassen sich Kurse - nur von Hand eintragen. + Nach kostenloser Anmeldung beim gewählten Dienst. Bei Yahoo Finanzen bleibt das Feld + leer; ohne Schlüssel lassen sich Kurse sonst nur von Hand eintragen.