diff --git a/modules/boersenchecker/assets/boersenchecker.js b/modules/boersenchecker/assets/boersenchecker.js index 5a098aa..c108095 100644 --- a/modules/boersenchecker/assets/boersenchecker.js +++ b/modules/boersenchecker/assets/boersenchecker.js @@ -115,7 +115,7 @@ } currentPayload = payload; renderChart(pointsForRange(payload, activeRange)); - if (statusNode) statusNode.textContent = `Quelle: Alpha Vantage | Symbol ${payload.symbol || ''}`; + if (statusNode) statusNode.textContent = `Quelle: Bavest | ISIN ${payload.isin || ''}`; } catch (error) { currentPayload = null; chartShell.innerHTML = `
${error.message}
`; diff --git a/modules/boersenchecker/bootstrap.php b/modules/boersenchecker/bootstrap.php index 7dbbc90..ac6b419 100644 --- a/modules/boersenchecker/bootstrap.php +++ b/modules/boersenchecker/bootstrap.php @@ -277,34 +277,56 @@ $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCu } }); -$mm->registerFunction($moduleName, 'alpha_vantage_fetch_quote', static function (string $symbol): array { +$mm->registerFunction($moduleName, 'bavest_request', static function ( + string $path, + array $payload = [], + string $accept = 'application/json', + string $method = 'POST' +): array { $settings = modules()->settings('boersenchecker'); - $apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? '')); - $timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12); + $apiKey = trim((string) ($settings['bavest_api_key'] ?? '')); + $timeout = (int) ($settings['bavest_timeout_sec'] ?? 12); $timeout = $timeout > 0 ? $timeout : 12; - $symbol = strtoupper(trim($symbol)); - - if ($symbol === '') { - return [ - 'ok' => false, - 'message' => 'Kein API-Symbol hinterlegt.', - ]; - } + $method = strtoupper(trim($method)); + $method = in_array($method, ['GET', 'POST'], true) ? $method : 'POST'; if ($apiKey === '') { return [ 'ok' => false, - 'message' => 'Alpha-Vantage-API-Key fehlt. Bitte im Modul-Setup hinterlegen.', + 'message' => 'Bavest-API-Key fehlt. Bitte im Modul-Setup hinterlegen.', ]; } - $url = 'https://www.alphavantage.co/query?' . http_build_query([ - 'function' => 'GLOBAL_QUOTE', - 'symbol' => $symbol, - 'apikey' => $apiKey, - ]); + $url = 'https://api.bavest.co/v2/' . ltrim($path, '/'); + $jsonPayload = ''; + if ($method === 'GET') { + if ($payload !== []) { + $query = http_build_query($payload, '', '&', PHP_QUERY_RFC3986); + if ($query !== '') { + $url .= (str_contains($url, '?') ? '&' : '?') . $query; + } + } + } else { + $jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($jsonPayload)) { + return [ + 'ok' => false, + 'message' => 'Bavest-Payload konnte nicht kodiert werden.', + ]; + } + } + + $headers = [ + 'Accept: ' . $accept, + 'x-api-key: ' . $apiKey, + ]; + if ($method === 'POST') { + $headers[] = 'Content-Type: application/json'; + } $responseBody = null; + $httpCode = 0; + $curlError = ''; if (function_exists('curl_init')) { $ch = curl_init($url); @@ -314,100 +336,216 @@ $mm->registerFunction($moduleName, 'alpha_vantage_fetch_quote', static function CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => min(5, $timeout), - CURLOPT_HTTPHEADER => ['Accept: application/json'], + CURLOPT_HTTPHEADER => $headers, ]); + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload); + } $responseBody = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); - - if (!is_string($responseBody) || $responseBody === '') { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Anfrage fehlgeschlagen.' - . ($curlError !== '' ? ' ' . $curlError : '') - . ($httpCode > 0 ? ' HTTP ' . $httpCode : ''), - ]; - } } } if (!is_string($responseBody) || $responseBody === '') { $context = stream_context_create([ 'http' => [ - 'method' => 'GET', + 'method' => $method, 'timeout' => $timeout, - 'header' => "Accept: application/json\r\n", + 'header' => implode("\r\n", $headers) . "\r\n", ], ]); - $responseBody = @file_get_contents($url, false, $context); - if (!is_string($responseBody) || $responseBody === '') { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Anfrage lieferte keine Daten.', - ]; + if ($method === 'POST') { + $contextOptions = stream_context_get_options($context); + $contextOptions['http']['content'] = $jsonPayload; + $context = stream_context_create($contextOptions); } + $responseBody = @file_get_contents($url, false, $context); + } + + if (!is_string($responseBody) || $responseBody === '') { + return [ + 'ok' => false, + 'message' => 'Bavest Anfrage fehlgeschlagen.' + . ($curlError !== '' ? ' ' . $curlError : '') + . ($httpCode > 0 ? ' HTTP ' . $httpCode : ''), + ]; } $decoded = json_decode($responseBody, true); if (!is_array($decoded)) { return [ 'ok' => false, - 'message' => 'Alpha Vantage Antwort ist kein gueltiges JSON.', + 'message' => 'Bavest Antwort ist kein gueltiges JSON.', + 'raw_body' => $responseBody, ]; } - if (!empty($decoded['Note'])) { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Limit-Hinweis: ' . trim((string) $decoded['Note']), - ]; - } - - if (!empty($decoded['Information'])) { - return [ - 'ok' => false, - 'message' => trim((string) $decoded['Information']), - ]; - } - - if (!empty($decoded['Error Message'])) { - return [ - 'ok' => false, - 'message' => trim((string) $decoded['Error Message']), - ]; - } - - $quote = is_array($decoded['Global Quote'] ?? null) ? $decoded['Global Quote'] : []; - $price = $quote['05. price'] ?? null; - if (!is_numeric($price)) { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage lieferte keinen Preis fuer das Symbol ' . $symbol . '.', - ]; + foreach (['error', 'message', 'detail'] as $errorKey) { + if (isset($decoded[$errorKey]) && is_string($decoded[$errorKey]) && trim($decoded[$errorKey]) !== '') { + return [ + 'ok' => false, + 'message' => trim((string) $decoded[$errorKey]), + 'raw' => $decoded, + ]; + } } return [ 'ok' => true, - 'symbol' => (string) ($quote['01. symbol'] ?? $symbol), - 'price' => (float) $price, - 'latest_trading_day' => (string) ($quote['07. latest trading day'] ?? ''), - 'previous_close' => is_numeric($quote['08. previous close'] ?? null) ? (float) $quote['08. previous close'] : null, - 'change' => is_numeric($quote['09. change'] ?? null) ? (float) $quote['09. change'] : null, - 'change_percent' => (string) ($quote['10. change percent'] ?? ''), - 'fetched_at' => date('Y-m-d H:i:s'), - 'source' => 'alpha_vantage:GLOBAL_QUOTE', - 'raw' => $quote, + 'data' => $decoded, ]; }); -$mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static function (string $keywords): array { - $settings = modules()->settings('boersenchecker'); - $apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? '')); - $timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12); - $timeout = $timeout > 0 ? $timeout : 12; - $keywords = trim($keywords); +$mm->registerFunction($moduleName, 'bavest_extract_quote', static function (array $entry): ?array { + $candidates = [$entry]; + foreach (['quote', 'data', 'result', 'security'] as $nestedKey) { + if (isset($entry[$nestedKey]) && is_array($entry[$nestedKey])) { + $candidates[] = $entry[$nestedKey]; + } + } + foreach ($candidates as $candidate) { + $price = null; + foreach (['price', 'close', 'last', 'lastPrice', 'currentPrice', 'c'] as $priceKey) { + if (is_numeric($candidate[$priceKey] ?? null)) { + $price = (float) $candidate[$priceKey]; + break; + } + } + if ($price === null) { + continue; + } + + $timestamp = trim((string) ($candidate['timestamp'] ?? $candidate['time'] ?? $candidate['date'] ?? '')); + $timestamp = $timestamp !== '' ? date('Y-m-d H:i:s', strtotime($timestamp) ?: time()) : date('Y-m-d H:i:s'); + + return [ + 'symbol' => trim((string) ($candidate['symbol'] ?? $candidate['ticker'] ?? $entry['symbol'] ?? '')), + 'isin' => trim((string) ($candidate['isin'] ?? $entry['isin'] ?? '')), + 'price' => $price, + 'currency' => strtoupper(trim((string) ($candidate['currency'] ?? $candidate['quoteCurrency'] ?? $entry['currency'] ?? 'EUR'))) ?: 'EUR', + 'fetched_at' => $timestamp, + 'source' => 'bavest:quote', + 'raw' => $candidate, + ]; + } + + return null; +}); + +$mm->registerFunction($moduleName, 'bavest_fetch_quote_by_isin', static function (string $isin): array { + $isin = strtoupper(trim($isin)); + if ($isin === '') { + return [ + 'ok' => false, + 'message' => 'Keine ISIN hinterlegt.', + ]; + } + + $response = module_fn('boersenchecker', 'bavest_request', 'timeseries/quote', ['isin' => $isin], 'application/json', 'GET'); + if (empty($response['ok'])) { + return $response; + } + + $data = is_array($response['data'] ?? null) ? $response['data'] : []; + if (isset($data['data']) && is_array($data['data'])) { + $data = [ + 'isin' => $isin, + 'data' => $data['data'], + ]; + } else { + $data['isin'] = $data['isin'] ?? $isin; + } + $quote = module_fn('boersenchecker', 'bavest_extract_quote', $data); + if (!is_array($quote)) { + return [ + 'ok' => false, + 'message' => 'Bavest lieferte keinen Preis fuer die ISIN ' . $isin . '.', + ]; + } + + return ['ok' => true] + $quote; +}); + +$mm->registerFunction($moduleName, 'bavest_fetch_bulk_quotes', static function (array $instruments): array { + $payloadSymbols = []; + $indexByIsin = []; + foreach ($instruments as $instrument) { + if (!is_array($instrument)) { + continue; + } + $isin = strtoupper(trim((string) ($instrument['isin'] ?? ''))); + if ($isin === '') { + continue; + } + $payloadSymbols[] = ['isin' => $isin]; + $indexByIsin[$isin] = (int) ($instrument['id'] ?? 0); + } + + if ($payloadSymbols === []) { + return [ + 'ok' => false, + 'message' => 'Keine ISIN fuer den Bulk-Abruf verfuegbar.', + 'quotes' => [], + ]; + } + + $response = module_fn('boersenchecker', 'bavest_request', 'bulk', [ + 'symbols' => $payloadSymbols, + 'endpoint' => 'quote', + 'params' => new stdClass(), + ], 'application/json', 'POST'); + if (empty($response['ok'])) { + return $response + ['quotes' => []]; + } + + $data = $response['data'] ?? []; + $items = []; + if (is_array($data)) { + if (isset($data['data']) && is_array($data['data'])) { + $items = $data['data']; + } elseif (isset($data['results']) && is_array($data['results'])) { + $items = $data['results']; + } elseif (array_is_list($data)) { + $items = $data; + } else { + $items = [$data]; + } + } + + $quotes = []; + foreach ($items as $offset => $item) { + if (!is_array($item)) { + continue; + } + $quote = module_fn('boersenchecker', 'bavest_extract_quote', $item); + if (!is_array($quote)) { + continue; + } + $isin = strtoupper(trim((string) ($quote['isin'] ?? $item['isin'] ?? ''))); + $instrumentId = $isin !== '' ? ($indexByIsin[$isin] ?? 0) : 0; + if ($instrumentId <= 0 && isset($payloadSymbols[$offset]['isin'])) { + $instrumentId = (int) ($indexByIsin[(string) $payloadSymbols[$offset]['isin']] ?? 0); + $quote['isin'] = (string) $payloadSymbols[$offset]['isin']; + } + if ($instrumentId <= 0) { + continue; + } + $quotes[$instrumentId] = $quote + ['instrument_id' => $instrumentId]; + } + + return [ + 'ok' => true, + 'quotes' => $quotes, + 'message' => count($quotes) . ' Kurse aus Bavest Bulk geladen.', + ]; +}); + +$mm->registerFunction($moduleName, 'bavest_search_symbols', static function (string $keywords): array { + $keywords = trim($keywords); if ($keywords === '') { return [ 'ok' => false, @@ -416,118 +554,60 @@ $mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static functi ]; } - if ($apiKey === '') { - return [ - 'ok' => false, - 'message' => 'Alpha-Vantage-API-Key fehlt. Bitte im Modul-Setup hinterlegen.', - 'results' => [], - ]; + $response = module_fn('boersenchecker', 'bavest_request', 'reference/search/aggregated', [ + 'q' => $keywords, + 'limit' => 25, + ], 'application/json', 'GET'); + if (empty($response['ok'])) { + return $response + ['results' => []]; } - $url = 'https://www.alphavantage.co/query?' . http_build_query([ - 'function' => 'SYMBOL_SEARCH', - 'keywords' => $keywords, - 'apikey' => $apiKey, - ]); - - $responseBody = null; - - if (function_exists('curl_init')) { - $ch = curl_init($url); - if ($ch !== false) { - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => $timeout, - CURLOPT_CONNECTTIMEOUT => min(5, $timeout), - CURLOPT_HTTPHEADER => ['Accept: application/json'], - ]); - $responseBody = curl_exec($ch); - $curlError = curl_error($ch); - $httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - curl_close($ch); - - if (!is_string($responseBody) || $responseBody === '') { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Suche fehlgeschlagen.' - . ($curlError !== '' ? ' ' . $curlError : '') - . ($httpCode > 0 ? ' HTTP ' . $httpCode : ''), - 'results' => [], - ]; - } - } + $data = $response['data'] ?? []; + $items = []; + if (is_array($data['data']['results'] ?? null)) { + $items = $data['data']['results']; + } elseif (is_array($data['results'] ?? null)) { + $items = $data['results']; } - if (!is_string($responseBody) || $responseBody === '') { - $context = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'timeout' => $timeout, - 'header' => "Accept: application/json\r\n", - ], - ]); - $responseBody = @file_get_contents($url, false, $context); - if (!is_string($responseBody) || $responseBody === '') { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Suche lieferte keine Daten.', - 'results' => [], - ]; - } - } - - $decoded = json_decode($responseBody, true); - if (!is_array($decoded)) { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Suchantwort ist kein gueltiges JSON.', - 'results' => [], - ]; - } - - if (!empty($decoded['Note'])) { - return [ - 'ok' => false, - 'message' => 'Alpha Vantage Limit-Hinweis: ' . trim((string) $decoded['Note']), - 'results' => [], - ]; - } - - if (!empty($decoded['Information'])) { - return [ - 'ok' => false, - 'message' => trim((string) $decoded['Information']), - 'results' => [], - ]; - } - - if (!empty($decoded['Error Message'])) { - return [ - 'ok' => false, - 'message' => trim((string) $decoded['Error Message']), - 'results' => [], - ]; - } - - $matches = is_array($decoded['bestMatches'] ?? null) ? $decoded['bestMatches'] : []; $results = []; - foreach ($matches as $match) { - if (!is_array($match)) { + foreach ($items as $item) { + if (!is_array($item)) { continue; } + $name = trim((string) ($item['name'] ?? $item['companyName'] ?? $item['securityName'] ?? '')); + $rootTicker = trim((string) ($item['root_ticker'] ?? '')); + $listings = is_array($item['listings'] ?? null) ? $item['listings'] : []; - $results[] = [ - 'symbol' => trim((string) ($match['1. symbol'] ?? '')), - 'name' => trim((string) ($match['2. name'] ?? '')), - 'type' => trim((string) ($match['3. type'] ?? '')), - 'region' => trim((string) ($match['4. region'] ?? '')), - 'market_open' => trim((string) ($match['5. marketOpen'] ?? '')), - 'market_close' => trim((string) ($match['6. marketClose'] ?? '')), - 'timezone' => trim((string) ($match['7. timezone'] ?? '')), - 'currency' => trim((string) ($match['8. currency'] ?? '')), - 'match_score' => trim((string) ($match['9. matchScore'] ?? '')), - ]; + foreach ($listings as $listing) { + if (!is_array($listing)) { + continue; + } + + $symbol = trim((string) ($listing['symbol'] ?? $rootTicker)); + $isin = strtoupper(trim((string) ($listing['isin'] ?? ''))); + $region = trim((string) ($listing['exchange'] ?? $listing['region'] ?? '')); + $type = trim((string) ($listing['type'] ?? '')); + $currency = strtoupper(trim((string) ($listing['currency'] ?? ''))); + + if ($symbol === '' && $name === '' && $isin === '') { + continue; + } + + $results[] = [ + 'symbol' => $symbol, + 'name' => $name, + 'isin' => $isin, + 'type' => $type, + 'region' => $region, + 'currency' => $currency, + 'match_score' => '', + 'raw' => [ + 'security' => $item, + 'listing' => $listing, + ], + ]; + } } return [ @@ -537,147 +617,89 @@ $mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static functi ]; }); -$mm->registerFunction($moduleName, 'alpha_vantage_fetch_chart_series', static function (string $symbol): array { - $settings = modules()->settings('boersenchecker'); - $apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? '')); - $timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12); - $timeout = $timeout > 0 ? $timeout : 12; - $symbol = strtoupper(trim($symbol)); - - if ($symbol === '') { - return ['ok' => false, 'message' => 'Kein Symbol angegeben.']; - } - if ($apiKey === '') { - return ['ok' => false, 'message' => 'Alpha-Vantage-API-Key fehlt.']; +$mm->registerFunction($moduleName, 'bavest_fetch_chart_series', static function (string $isin): array { + $isin = strtoupper(trim($isin)); + if ($isin === '') { + return ['ok' => false, 'message' => 'Keine ISIN angegeben.']; } - $cacheDir = sys_get_temp_dir() . '/boersenchecker-alpha-vantage'; + $cacheDir = sys_get_temp_dir() . '/boersenchecker-bavest'; if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0775, true); } + $cachePath = $cacheDir . '/' . md5('historical-price|' . $isin) . '.json'; - $fetchPayload = static function (string $functionName, int $ttl) use ($symbol, $apiKey, $timeout, $cacheDir): array { - $cacheKey = md5($functionName . '|' . $symbol . '|' . $apiKey); - $cachePath = $cacheDir . '/' . $cacheKey . '.json'; - if (is_file($cachePath) && (time() - filemtime($cachePath)) < $ttl) { - $cached = file_get_contents($cachePath); - $decoded = is_string($cached) ? json_decode($cached, true) : null; - if (is_array($decoded)) { - return $decoded; - } - } - - $url = 'https://www.alphavantage.co/query?' . http_build_query([ - 'function' => $functionName, - 'symbol' => $symbol, - 'apikey' => $apiKey, - 'outputsize' => 'compact', - ]); - - $responseBody = null; - if (function_exists('curl_init')) { - $ch = curl_init($url); - if ($ch !== false) { - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => $timeout, - CURLOPT_CONNECTTIMEOUT => min(5, $timeout), - CURLOPT_HTTPHEADER => ['Accept: application/json'], - ]); - $responseBody = curl_exec($ch); - curl_close($ch); - } - } - - if (!is_string($responseBody) || $responseBody === '') { - $context = stream_context_create([ - 'http' => [ - 'method' => 'GET', - 'timeout' => $timeout, - 'header' => "Accept: application/json\r\n", - ], - ]); - $responseBody = @file_get_contents($url, false, $context); - } - - $decoded = is_string($responseBody) ? json_decode($responseBody, true) : null; - if (is_array($decoded) && $decoded !== []) { - @file_put_contents($cachePath, json_encode($decoded, JSON_UNESCAPED_UNICODE)); - return $decoded; - } - - return []; - }; - - $normalizeSeries = static function (array $payload, array $keys): array { - foreach ($keys as $key) { - $series = $payload[$key] ?? null; - if (!is_array($series)) { - continue; - } - - $points = []; - foreach ($series as $date => $row) { - if (!is_array($row)) { - continue; - } - $close = $row['4. close'] ?? $row['5. adjusted close'] ?? null; - if (!is_numeric($close)) { - $close = $row['5. adjusted close'] ?? $row['4. close'] ?? null; - } - if (!is_numeric($close)) { - continue; - } - $points[] = [ - 'date' => (string) $date, - 'close' => (float) $close, - ]; - } - - usort($points, static fn (array $left, array $right): int => strcmp((string) $left['date'], (string) $right['date'])); - return $points; - } - - return []; - }; - - $dailyPayload = $fetchPayload('TIME_SERIES_DAILY_ADJUSTED', 6 * 3600); - if (($dailyPayload['Information'] ?? null) || ($dailyPayload['Error Message'] ?? null)) { - $dailyPayload = $fetchPayload('TIME_SERIES_DAILY', 6 * 3600); - } - $weeklyPayload = $fetchPayload('TIME_SERIES_WEEKLY_ADJUSTED', 12 * 3600); - if (($weeklyPayload['Information'] ?? null) || ($weeklyPayload['Error Message'] ?? null)) { - $weeklyPayload = $fetchPayload('TIME_SERIES_WEEKLY', 12 * 3600); - } - $monthlyPayload = $fetchPayload('TIME_SERIES_MONTHLY_ADJUSTED', 24 * 3600); - if (($monthlyPayload['Information'] ?? null) || ($monthlyPayload['Error Message'] ?? null)) { - $monthlyPayload = $fetchPayload('TIME_SERIES_MONTHLY', 24 * 3600); + $decoded = null; + if (is_file($cachePath) && (time() - filemtime($cachePath)) < (6 * 3600)) { + $cached = file_get_contents($cachePath); + $decoded = is_string($cached) ? json_decode($cached, true) : null; } - if (!empty($dailyPayload['Note']) || !empty($weeklyPayload['Note']) || !empty($monthlyPayload['Note'])) { + if (!is_array($decoded)) { + $response = module_fn('boersenchecker', 'bavest_request', 'timeseries/history', [ + 'isin' => $isin, + 'from' => date('Y-m-d', strtotime('-6 years')), + 'to' => date('Y-m-d'), + ], 'application/json', 'GET'); + if (empty($response['ok'])) { + return $response; + } + $decoded = is_array($response['data'] ?? null) ? $response['data'] : []; + @file_put_contents($cachePath, json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + } + + $rows = []; + if (isset($decoded['data']['prices']) && is_array($decoded['data']['prices'])) { + $rows = $decoded['data']['prices']; + } elseif (isset($decoded['data']) && is_array($decoded['data'])) { + $rows = $decoded['data']; + } elseif (isset($decoded['results']) && is_array($decoded['results'])) { + $rows = $decoded['results']; + } elseif (array_is_list($decoded)) { + $rows = $decoded; + } + + $dailyByDate = []; + foreach ($rows as $row) { + if (!is_array($row)) { + continue; + } + $date = trim((string) ($row['date'] ?? $row['time'] ?? $row['timestamp'] ?? '')); + $close = $row['close'] ?? $row['price'] ?? $row['c'] ?? null; + if ($date === '' || !is_numeric($close)) { + continue; + } + $normalizedDate = date('Y-m-d', strtotime($date) ?: time()); + $dailyByDate[$normalizedDate] = [ + 'date' => date('Y-m-d', strtotime($date) ?: time()), + 'close' => (float) $close, + ]; + } + $daily = array_values($dailyByDate); + + usort($daily, static fn (array $left, array $right): int => strcmp((string) $left['date'], (string) $right['date'])); + if ($daily === []) { return [ 'ok' => false, - 'message' => 'Alpha Vantage Limit erreicht. Bitte spaeter erneut versuchen.', + 'message' => 'Keine historischen Schlusskurse fuer ' . $isin . ' verfuegbar.', ]; } - $daily = $normalizeSeries($dailyPayload, ['Time Series (Daily)']); - $weekly = $normalizeSeries($weeklyPayload, ['Weekly Adjusted Time Series', 'Weekly Time Series']); - $monthly = $normalizeSeries($monthlyPayload, ['Monthly Adjusted Time Series', 'Monthly Time Series']); - - if ($daily === [] && $weekly === [] && $monthly === []) { - return [ - 'ok' => false, - 'message' => 'Keine Zeitreihendaten fuer ' . $symbol . ' verfuegbar.', - ]; - } + $aggregate = static function (array $points, string $format): array { + $result = []; + foreach ($points as $point) { + $bucket = date($format, strtotime((string) $point['date']) ?: time()); + $result[$bucket] = $point; + } + return array_values($result); + }; return [ 'ok' => true, - 'symbol' => $symbol, + 'isin' => $isin, 'daily' => $daily, - 'weekly' => $weekly, - 'monthly' => $monthly, + 'weekly' => $aggregate($daily, 'o-W'), + 'monthly' => $aggregate($daily, 'Y-m'), + 'source' => 'bavest:timeseries/history', ]; }); diff --git a/modules/boersenchecker/module.json b/modules/boersenchecker/module.json index 87b9d72..2d4bc25 100644 --- a/modules/boersenchecker/module.json +++ b/modules/boersenchecker/module.json @@ -15,9 +15,9 @@ { "name": "db.password", "label": "DB Passwort", "type": "password", "required": false }, { "name": "report_currency", "label": "Standard-Berichtswahrung", "type": "text", "required": false, "help": "Zielwaehrung fuer Portfolio-Summen, z.B. EUR." }, { "name": "fx_max_age_hours", "label": "Maximales FX-Alter (Stunden)", "type": "number", "required": false, "help": "Wird bei manueller Aktualisierung ueber den Mining-Checker genutzt." }, - { "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe ueber GLOBAL_QUOTE." }, - { "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." }, - { "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." } + { "name": "bavest_api_key", "label": "Bavest API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Bavest." }, + { "name": "bavest_timeout_sec", "label": "Bavest Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." }, + { "name": "bavest_min_interval_minutes", "label": "Bavest Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Bavest-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." } ] }, "db_defaults": { diff --git a/modules/boersenchecker/pages/chart_data.php b/modules/boersenchecker/pages/chart_data.php index 04ac1f4..808dc26 100644 --- a/modules/boersenchecker/pages/chart_data.php +++ b/modules/boersenchecker/pages/chart_data.php @@ -19,7 +19,7 @@ $instrumentTable = module_fn('boersenchecker', 'table', 'instruments'); $positionTable = module_fn('boersenchecker', 'table', 'positions'); $stmt = $pdo->prepare( - 'SELECT i.id, i.name, i.symbol + 'SELECT i.id, i.name, i.symbol, i.isin FROM ' . $instrumentTable . ' i INNER JOIN ' . $positionTable . ' p ON p.instrument_id = i.id WHERE i.id = :id AND p.owner_sub = :owner_sub @@ -37,12 +37,12 @@ if (!is_array($instrument)) { exit; } -$symbol = trim((string) ($instrument['symbol'] ?? '')); -if ($symbol === '') { - echo json_encode(['ok' => false, 'message' => 'Fuer diese Aktie ist kein Symbol hinterlegt.'], JSON_UNESCAPED_UNICODE); +$isin = strtoupper(trim((string) ($instrument['isin'] ?? ''))); +if ($isin === '') { + echo json_encode(['ok' => false, 'message' => 'Fuer diese Aktie ist keine ISIN hinterlegt.'], JSON_UNESCAPED_UNICODE); exit; } -$result = module_fn('boersenchecker', 'alpha_vantage_fetch_chart_series', $symbol); +$result = module_fn('boersenchecker', 'bavest_fetch_chart_series', $isin); echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; diff --git a/modules/boersenchecker/partials/dashboard.php b/modules/boersenchecker/partials/dashboard.php index 8c401c1..73bdd18 100644 --- a/modules/boersenchecker/partials/dashboard.php +++ b/modules/boersenchecker/partials/dashboard.php @@ -81,7 +81,7 @@ angestossen und respektiert die dortige Max-Age-Logik.

- Aktienkurse werden ueber Alpha Vantage anhand des hinterlegten API-Symbols / Tickers pro Aktie abgerufen. + Aktienkurse werden ueber Bavest per ISIN abgerufen. Das Ticker-Symbol bleibt als Zusatzinformation erhalten.

@@ -90,13 +90,13 @@
- +
- Alpha Vantage Mindestabstand: Min. + Bavest Mindestabstand: Min.
API-Key und Timeout werden ueber Modul-Setup gepflegt. @@ -198,8 +198,8 @@
-

API-Symbol suchen

-

Alpha-Vantage-Symbole suchen und direkt ins Positionsformular uebernehmen.

+

Wertpapiersuche

+

Bavest-Suchergebnisse pruefen und Daten direkt ins Positionsformular uebernehmen.

@@ -237,7 +237,7 @@ - + In Formular uebernehmen @@ -428,7 +428,7 @@ Bearbeiten Kurs erfassen
- + diff --git a/modules/boersenchecker/partials/home.php b/modules/boersenchecker/partials/home.php index d5d425c..242f921 100644 --- a/modules/boersenchecker/partials/home.php +++ b/modules/boersenchecker/partials/home.php @@ -68,7 +68,7 @@
Marktdaten
-

Aktuelle Kurse fuer das gewaehlte Depot ueber Alpha Vantage abrufen.

+

Aktuelle Kurse fuer das gewaehlte Depot ueber Bavest per ISIN abrufen.

diff --git a/modules/boersenchecker/partials/instruments.php b/modules/boersenchecker/partials/instruments.php index 1ed3bb5..51a22f2 100644 --- a/modules/boersenchecker/partials/instruments.php +++ b/modules/boersenchecker/partials/instruments.php @@ -36,8 +36,8 @@
-

Symbolsuche

-

Alpha-Vantage-Symbole finden und direkt fuer die Aktie uebernehmen.

+

Wertpapiersuche

+

Bavest-Suchergebnisse finden und direkt fuer die Aktie uebernehmen.

@@ -70,7 +70,7 @@ - + Uebernehmen @@ -112,7 +112,7 @@
- +
diff --git a/modules/boersenchecker/src/Support/DashboardPage.php b/modules/boersenchecker/src/Support/DashboardPage.php index b257ecb..3c51643 100644 --- a/modules/boersenchecker/src/Support/DashboardPage.php +++ b/modules/boersenchecker/src/Support/DashboardPage.php @@ -15,7 +15,7 @@ final class DashboardPage private array $moduleSettings; private string $defaultReportCurrency; private float $fxMaxAgeHours; - private int $alphaMinIntervalMinutes; + private int $marketDataMinIntervalMinutes; private int $editPortfolioId; private int $editPositionId; private string $portfolioTable; @@ -49,9 +49,9 @@ final class DashboardPage $this->fxMaxAgeHours = 6.0; } - $this->alphaMinIntervalMinutes = (int) ($this->moduleSettings['alpha_vantage_min_interval_minutes'] ?? 60); - if ($this->alphaMinIntervalMinutes <= 0) { - $this->alphaMinIntervalMinutes = 60; + $this->marketDataMinIntervalMinutes = (int) ($this->moduleSettings['bavest_min_interval_minutes'] ?? 60); + if ($this->marketDataMinIntervalMinutes <= 0) { + $this->marketDataMinIntervalMinutes = 60; } $this->editPortfolioId = (int) ($_GET['edit_portfolio'] ?? 0); @@ -100,7 +100,7 @@ final class DashboardPage 'availableOwners' => array_values($this->availableOwners), 'defaultReportCurrency' => $this->defaultReportCurrency, 'fxMaxAgeHours' => $this->fxMaxAgeHours, - 'alphaMinIntervalMinutes' => $this->alphaMinIntervalMinutes, + 'marketDataMinIntervalMinutes' => $this->marketDataMinIntervalMinutes, 'symbolSearchKeywords' => $this->symbolSearchKeywords, 'symbolSearchResults' => $this->symbolSearchResults, 'editPortfolio' => $state['editPortfolio'], @@ -127,8 +127,8 @@ final class DashboardPage 'save_position' => $this->savePosition(), 'delete_position' => $this->deletePosition(), 'save_quote' => $this->saveQuote(), - 'refresh_alpha_vantage_position' => $this->refreshAlphaVantagePosition(), - 'refresh_alpha_vantage_all' => $this->refreshAlphaVantageAll(), + 'refresh_market_data_position' => $this->refreshMarketDataPosition(), + 'refresh_market_data_all' => $this->refreshMarketDataAll(), 'search_symbol' => $this->searchSymbol(), 'delete_quote' => $this->deleteQuote(), 'refresh_fx' => $this->refreshFx(), @@ -172,6 +172,7 @@ final class DashboardPage $candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? '')); $candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? '')); + $candidateIsin = trim((string) ($_GET['isin_candidate'] ?? '')); $candidateMarket = trim((string) ($_GET['market_candidate'] ?? '')); $candidateCurrency = $this->normalizeCurrency((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency)); @@ -179,6 +180,7 @@ final class DashboardPage $editPosition = [ 'instrument_name' => $candidateName, 'symbol' => $candidateSymbol, + 'isin' => $candidateIsin, 'market' => $candidateMarket, 'quote_currency' => $candidateCurrency, 'purchase_currency' => $this->defaultReportCurrency, @@ -191,6 +193,9 @@ final class DashboardPage if ($candidateSymbol !== '') { $editPosition['symbol'] = $candidateSymbol; } + if ($candidateIsin !== '') { + $editPosition['isin'] = $candidateIsin; + } if ($candidateMarket !== '') { $editPosition['market'] = $candidateMarket; } @@ -378,7 +383,7 @@ final class DashboardPage return 'Kurs gespeichert.'; } - private function refreshAlphaVantagePosition(): string + private function refreshMarketDataPosition(): string { $positionId = (int) ($_POST['position_id'] ?? 0); $stmt = $this->pdo->prepare( @@ -386,6 +391,7 @@ final class DashboardPage p.instrument_id, i.name AS instrument_name, i.symbol, + i.isin, i.quote_currency FROM ' . $this->positionTable . ' p INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id @@ -402,19 +408,19 @@ final class DashboardPage } $instrumentId = (int) $row['instrument_id']; - $symbol = strtoupper(trim((string) ($row['symbol'] ?? ''))); + $isin = strtoupper(trim((string) ($row['isin'] ?? ''))); $quoteCurrency = $this->normalizeCurrency((string) ($row['quote_currency'] ?? $this->defaultReportCurrency)); - if ($symbol === '') { - throw new RuntimeException('Fuer diese Aktie ist noch kein API-Symbol / Ticker hinterlegt.'); + if ($isin === '') { + throw new RuntimeException('Fuer diese Aktie ist noch keine ISIN hinterlegt.'); } $latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId); $latestTimestamp = is_array($latestApiQuote) ? strtotime((string) ($latestApiQuote['quoted_at'] ?? '')) : false; - if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->alphaMinIntervalMinutes * 60)) { - return 'Vorhandener Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' wiederverwendet.'; + if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->marketDataMinIntervalMinutes * 60)) { + return 'Vorhandener Bavest-Kurs fuer ' . (string) $row['instrument_name'] . ' wiederverwendet.'; } - $apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol); + $apiResult = \module_fn('boersenchecker', 'bavest_fetch_quote_by_isin', $isin); if (empty($apiResult['ok'])) { throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); } @@ -426,16 +432,17 @@ final class DashboardPage (string) $apiResult['fetched_at'], (string) $apiResult['source'] ); - return 'API-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.'; + return 'Bavest-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.'; } - private function refreshAlphaVantageAll(): string + private function refreshMarketDataAll(): string { $stmt = $this->pdo->prepare( 'SELECT DISTINCT i.id, i.name, i.symbol, + i.isin, i.quote_currency FROM ' . $this->positionTable . ' p INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id @@ -454,58 +461,68 @@ final class DashboardPage $failed = 0; $errors = []; + $bulkRows = []; foreach ($rows as $row) { $instrumentId = (int) ($row['id'] ?? 0); - $symbol = strtoupper(trim((string) ($row['symbol'] ?? ''))); - $quoteCurrency = $this->normalizeCurrency((string) ($row['quote_currency'] ?? $this->defaultReportCurrency)); - - if ($instrumentId <= 0 || $symbol === '') { + $isin = strtoupper(trim((string) ($row['isin'] ?? ''))); + if ($instrumentId <= 0 || $isin === '') { $skipped++; continue; } $latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId); $latestTimestamp = is_array($latestApiQuote) ? strtotime((string) ($latestApiQuote['quoted_at'] ?? '')) : false; - if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->alphaMinIntervalMinutes * 60)) { + if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->marketDataMinIntervalMinutes * 60)) { $reused++; continue; } - $apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol); - if (empty($apiResult['ok'])) { - $failed++; - $errors[] = (string) ($row['name'] ?? $symbol) . ': ' . (string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'); - if (stripos((string) ($apiResult['message'] ?? ''), 'limit') !== false) { - break; - } - continue; + $bulkRows[] = $row; + } + + if ($bulkRows !== []) { + $bulkResult = \module_fn('boersenchecker', 'bavest_fetch_bulk_quotes', $bulkRows); + if (empty($bulkResult['ok'])) { + throw new RuntimeException((string) ($bulkResult['message'] ?? 'Bavest Bulk-Abruf fehlgeschlagen.')); } - $this->storeQuote( - $instrumentId, - (float) $apiResult['price'], - $quoteCurrency, - (string) $apiResult['fetched_at'], - (string) $apiResult['source'] - ); - $fetched++; + $bulkQuotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : []; + foreach ($bulkRows as $row) { + $instrumentId = (int) ($row['id'] ?? 0); + $quoteCurrency = $this->normalizeCurrency((string) ($row['quote_currency'] ?? $this->defaultReportCurrency)); + $apiResult = $bulkQuotes[$instrumentId] ?? null; + if (!is_array($apiResult) || !is_numeric($apiResult['price'] ?? null)) { + $failed++; + $errors[] = (string) ($row['name'] ?? $instrumentId) . ': kein Preis in der Bavest-Antwort.'; + continue; + } + + $this->storeQuote( + $instrumentId, + (float) $apiResult['price'], + $quoteCurrency, + (string) $apiResult['fetched_at'], + (string) $apiResult['source'] + ); + $fetched++; + } } if ($errors !== []) { throw new RuntimeException( - 'Alpha Vantage: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne Symbol, ' . $failed . ' Fehler. ' + 'Bavest: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne ISIN, ' . $failed . ' Fehler. ' . implode(' | ', array_slice($errors, 0, 3)) ); } - return 'Alpha Vantage: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne Symbol, ' . $failed . ' Fehler.'; + return 'Bavest: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne ISIN, ' . $failed . ' Fehler.'; } private function searchSymbol(): string { $keywords = trim((string) ($_POST['search_keywords'] ?? '')); $this->symbolSearchKeywords = $keywords; - $result = \module_fn('boersenchecker', 'alpha_vantage_search_symbols', $keywords); + $result = \module_fn('boersenchecker', 'bavest_search_symbols', $keywords); $this->symbolSearchResults = is_array($result['results'] ?? null) ? $result['results'] : []; if (empty($result['ok'])) { @@ -708,7 +725,7 @@ final class DashboardPage ); $stmt->execute([ 'instrument_id' => $instrumentId, - 'source' => 'alpha_vantage:%', + 'source' => 'bavest:%', ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return is_array($row) ? $row : null; diff --git a/modules/boersenchecker/src/Support/HomePage.php b/modules/boersenchecker/src/Support/HomePage.php index b614043..3eec9cd 100644 --- a/modules/boersenchecker/src/Support/HomePage.php +++ b/modules/boersenchecker/src/Support/HomePage.php @@ -15,7 +15,7 @@ final class HomePage private string $positionTable; private string $quoteTable; private string $defaultReportCurrency; - private int $alphaMinIntervalMinutes; + private int $marketDataMinIntervalMinutes; public function __construct() { @@ -26,9 +26,9 @@ final class HomePage $settings = \modules()->settings('boersenchecker'); $this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR'; - $this->alphaMinIntervalMinutes = (int) ($settings['alpha_vantage_min_interval_minutes'] ?? 60); - if ($this->alphaMinIntervalMinutes <= 0) { - $this->alphaMinIntervalMinutes = 60; + $this->marketDataMinIntervalMinutes = (int) ($settings['bavest_min_interval_minutes'] ?? 60); + if ($this->marketDataMinIntervalMinutes <= 0) { + $this->marketDataMinIntervalMinutes = 60; } $table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name); @@ -118,7 +118,7 @@ final class HomePage } $stmt = $this->pdo->prepare( - 'SELECT DISTINCT i.id, i.name, i.symbol, i.quote_currency + 'SELECT DISTINCT i.id, i.name, i.symbol, i.isin, i.quote_currency FROM ' . $this->positionTable . ' p INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id WHERE p.owner_sub = :owner_sub AND p.portfolio_id = :portfolio_id' @@ -134,37 +134,47 @@ final class HomePage $updated = 0; $reused = 0; + $bulkCandidates = []; foreach ($rows as $row) { $instrumentId = (int) ($row['id'] ?? 0); - $symbol = strtoupper(trim((string) ($row['symbol'] ?? ''))); - if ($instrumentId <= 0 || $symbol === '') { + $isin = strtoupper(trim((string) ($row['isin'] ?? ''))); + if ($instrumentId <= 0 || $isin === '') { continue; } $latest = $this->latestApiQuoteForInstrument($instrumentId); $latestTimestamp = is_array($latest) ? strtotime((string) ($latest['quoted_at'] ?? '')) : false; - if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->alphaMinIntervalMinutes * 60)) { + if ($latestTimestamp !== false && (time() - $latestTimestamp) < ($this->marketDataMinIntervalMinutes * 60)) { $reused++; continue; } - $apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol); - if (empty($apiResult['ok'])) { - continue; - } + $bulkCandidates[] = $row; + } + if ($bulkCandidates !== []) { + $bulkResult = \module_fn('boersenchecker', 'bavest_fetch_bulk_quotes', $bulkCandidates); + $quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : []; $stmtInsert = $this->pdo->prepare( 'INSERT INTO ' . $this->quoteTable . ' (instrument_id, price, currency, quoted_at, source) VALUES (:instrument_id, :price, :currency, :quoted_at, :source)' ); - $stmtInsert->execute([ - 'instrument_id' => $instrumentId, - 'price' => (float) $apiResult['price'], - 'currency' => strtoupper(trim((string) ($row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, - 'quoted_at' => (string) $apiResult['fetched_at'], - 'source' => (string) $apiResult['source'], - ]); - $updated++; + + foreach ($bulkCandidates as $row) { + $instrumentId = (int) ($row['id'] ?? 0); + $quote = $quotes[$instrumentId] ?? null; + if (!is_array($quote) || !is_numeric($quote['price'] ?? null)) { + continue; + } + $stmtInsert->execute([ + 'instrument_id' => $instrumentId, + 'price' => (float) $quote['price'], + 'currency' => strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency, + 'quoted_at' => (string) ($quote['fetched_at'] ?? date('Y-m-d H:i:s')), + 'source' => (string) ($quote['source'] ?? 'bavest:quote'), + ]); + $updated++; + } } return 'Aktuelle Kurse: ' . $updated . ' aktualisiert, ' . $reused . ' wiederverwendet.'; @@ -241,7 +251,7 @@ final class HomePage ); $stmt->execute([ 'instrument_id' => $instrumentId, - 'source' => 'alpha_vantage:%', + 'source' => 'bavest:%', ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); return is_array($row) ? $row : null; diff --git a/modules/boersenchecker/src/Support/InstrumentPage.php b/modules/boersenchecker/src/Support/InstrumentPage.php index 579e7ea..efb2794 100644 --- a/modules/boersenchecker/src/Support/InstrumentPage.php +++ b/modules/boersenchecker/src/Support/InstrumentPage.php @@ -72,6 +72,7 @@ final class InstrumentPage $candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? '')); $candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? '')); + $candidateIsin = trim((string) ($_GET['isin_candidate'] ?? '')); $candidateMarket = trim((string) ($_GET['market_candidate'] ?? '')); $candidateCurrency = strtoupper(trim((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency; if ($selectedInstrument === null && ($candidateName !== '' || $candidateSymbol !== '' || $candidateMarket !== '')) { @@ -79,9 +80,9 @@ final class InstrumentPage 'id' => 0, 'name' => $candidateName, 'symbol' => $candidateSymbol, + 'isin' => $candidateIsin, 'market' => $candidateMarket, 'quote_currency' => $candidateCurrency, - 'isin' => '', 'wkn' => '', ]; } @@ -106,7 +107,7 @@ final class InstrumentPage 'save_instrument' => $this->saveInstrument(), 'save_quote' => $this->saveQuote(), 'delete_quote' => $this->deleteQuote(), - 'refresh_alpha_vantage_instrument' => $this->refreshInstrumentQuote(), + 'refresh_market_data_instrument' => $this->refreshInstrumentQuote(), 'search_symbol' => $this->searchSymbol(), default => '', }; @@ -211,12 +212,12 @@ final class InstrumentPage { $instrumentId = (int) ($_POST['instrument_id'] ?? 0); $instrument = $this->assertInstrumentAccessible($instrumentId); - $symbol = trim((string) ($instrument['symbol'] ?? '')); - if ($symbol === '') { - throw new RuntimeException('Fuer diese Aktie ist kein Symbol hinterlegt.'); + $isin = strtoupper(trim((string) ($instrument['isin'] ?? ''))); + if ($isin === '') { + throw new RuntimeException('Fuer diese Aktie ist keine ISIN hinterlegt.'); } - $apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol); + $apiResult = \module_fn('boersenchecker', 'bavest_fetch_quote_by_isin', $isin); if (empty($apiResult['ok'])) { throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.')); } @@ -233,13 +234,13 @@ final class InstrumentPage 'source' => (string) $apiResult['source'], ]); - return 'API-Kurs gespeichert.'; + return 'Bavest-Kurs gespeichert.'; } private function searchSymbol(): string { $this->searchKeywords = trim((string) ($_POST['search_keywords'] ?? '')); - $result = \module_fn('boersenchecker', 'alpha_vantage_search_symbols', $this->searchKeywords); + $result = \module_fn('boersenchecker', 'bavest_search_symbols', $this->searchKeywords); $this->searchResults = is_array($result['results'] ?? null) ? $result['results'] : []; if (empty($result['ok'])) { throw new RuntimeException((string) ($result['message'] ?? 'Symbolsuche fehlgeschlagen.'));