umstellung
This commit is contained in:
@@ -115,7 +115,7 @@
|
|||||||
}
|
}
|
||||||
currentPayload = payload;
|
currentPayload = payload;
|
||||||
renderChart(pointsForRange(payload, activeRange));
|
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) {
|
} catch (error) {
|
||||||
currentPayload = null;
|
currentPayload = null;
|
||||||
chartShell.innerHTML = `<div class="muted">${error.message}</div>`;
|
chartShell.innerHTML = `<div class="muted">${error.message}</div>`;
|
||||||
|
|||||||
@@ -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');
|
$settings = modules()->settings('boersenchecker');
|
||||||
$apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? ''));
|
$apiKey = trim((string) ($settings['bavest_api_key'] ?? ''));
|
||||||
$timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12);
|
$timeout = (int) ($settings['bavest_timeout_sec'] ?? 12);
|
||||||
$timeout = $timeout > 0 ? $timeout : 12;
|
$timeout = $timeout > 0 ? $timeout : 12;
|
||||||
$symbol = strtoupper(trim($symbol));
|
$method = strtoupper(trim($method));
|
||||||
|
$method = in_array($method, ['GET', 'POST'], true) ? $method : 'POST';
|
||||||
if ($symbol === '') {
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'message' => 'Kein API-Symbol hinterlegt.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($apiKey === '') {
|
if ($apiKey === '') {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'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([
|
$url = 'https://api.bavest.co/v2/' . ltrim($path, '/');
|
||||||
'function' => 'GLOBAL_QUOTE',
|
$jsonPayload = '';
|
||||||
'symbol' => $symbol,
|
if ($method === 'GET') {
|
||||||
'apikey' => $apiKey,
|
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;
|
$responseBody = null;
|
||||||
|
$httpCode = 0;
|
||||||
|
$curlError = '';
|
||||||
|
|
||||||
if (function_exists('curl_init')) {
|
if (function_exists('curl_init')) {
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
@@ -314,100 +336,216 @@ $mm->registerFunction($moduleName, 'alpha_vantage_fetch_quote', static function
|
|||||||
CURLOPT_FOLLOWLOCATION => true,
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
CURLOPT_TIMEOUT => $timeout,
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
CURLOPT_CONNECTTIMEOUT => min(5, $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);
|
$responseBody = curl_exec($ch);
|
||||||
$curlError = curl_error($ch);
|
$curlError = curl_error($ch);
|
||||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
curl_close($ch);
|
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 === '') {
|
if (!is_string($responseBody) || $responseBody === '') {
|
||||||
$context = stream_context_create([
|
$context = stream_context_create([
|
||||||
'http' => [
|
'http' => [
|
||||||
'method' => 'GET',
|
'method' => $method,
|
||||||
'timeout' => $timeout,
|
'timeout' => $timeout,
|
||||||
'header' => "Accept: application/json\r\n",
|
'header' => implode("\r\n", $headers) . "\r\n",
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
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);
|
$responseBody = @file_get_contents($url, false, $context);
|
||||||
|
}
|
||||||
|
|
||||||
if (!is_string($responseBody) || $responseBody === '') {
|
if (!is_string($responseBody) || $responseBody === '') {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'message' => 'Alpha Vantage Anfrage lieferte keine Daten.',
|
'message' => 'Bavest Anfrage fehlgeschlagen.'
|
||||||
|
. ($curlError !== '' ? ' ' . $curlError : '')
|
||||||
|
. ($httpCode > 0 ? ' HTTP ' . $httpCode : ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($responseBody, true);
|
$decoded = json_decode($responseBody, true);
|
||||||
if (!is_array($decoded)) {
|
if (!is_array($decoded)) {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'message' => 'Alpha Vantage Antwort ist kein gueltiges JSON.',
|
'message' => 'Bavest Antwort ist kein gueltiges JSON.',
|
||||||
|
'raw_body' => $responseBody,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($decoded['Note'])) {
|
foreach (['error', 'message', 'detail'] as $errorKey) {
|
||||||
|
if (isset($decoded[$errorKey]) && is_string($decoded[$errorKey]) && trim($decoded[$errorKey]) !== '') {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'message' => 'Alpha Vantage Limit-Hinweis: ' . trim((string) $decoded['Note']),
|
'message' => trim((string) $decoded[$errorKey]),
|
||||||
|
'raw' => $decoded,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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 . '.',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'symbol' => (string) ($quote['01. symbol'] ?? $symbol),
|
'data' => $decoded,
|
||||||
'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,
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static function (string $keywords): array {
|
$mm->registerFunction($moduleName, 'bavest_extract_quote', static function (array $entry): ?array {
|
||||||
$settings = modules()->settings('boersenchecker');
|
$candidates = [$entry];
|
||||||
$apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? ''));
|
foreach (['quote', 'data', 'result', 'security'] as $nestedKey) {
|
||||||
$timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12);
|
if (isset($entry[$nestedKey]) && is_array($entry[$nestedKey])) {
|
||||||
$timeout = $timeout > 0 ? $timeout : 12;
|
$candidates[] = $entry[$nestedKey];
|
||||||
$keywords = trim($keywords);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === '') {
|
if ($keywords === '') {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
@@ -416,119 +554,61 @@ $mm->registerFunction($moduleName, 'alpha_vantage_search_symbols', static functi
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($apiKey === '') {
|
$response = module_fn('boersenchecker', 'bavest_request', 'reference/search/aggregated', [
|
||||||
return [
|
'q' => $keywords,
|
||||||
'ok' => false,
|
'limit' => 25,
|
||||||
'message' => 'Alpha-Vantage-API-Key fehlt. Bitte im Modul-Setup hinterlegen.',
|
], 'application/json', 'GET');
|
||||||
'results' => [],
|
if (empty($response['ok'])) {
|
||||||
];
|
return $response + ['results' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = 'https://www.alphavantage.co/query?' . http_build_query([
|
$data = $response['data'] ?? [];
|
||||||
'function' => 'SYMBOL_SEARCH',
|
$items = [];
|
||||||
'keywords' => $keywords,
|
if (is_array($data['data']['results'] ?? null)) {
|
||||||
'apikey' => $apiKey,
|
$items = $data['data']['results'];
|
||||||
]);
|
} elseif (is_array($data['results'] ?? null)) {
|
||||||
|
$items = $data['results'];
|
||||||
$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' => [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = [];
|
$results = [];
|
||||||
foreach ($matches as $match) {
|
foreach ($items as $item) {
|
||||||
if (!is_array($match)) {
|
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'] : [];
|
||||||
|
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$results[] = [
|
$results[] = [
|
||||||
'symbol' => trim((string) ($match['1. symbol'] ?? '')),
|
'symbol' => $symbol,
|
||||||
'name' => trim((string) ($match['2. name'] ?? '')),
|
'name' => $name,
|
||||||
'type' => trim((string) ($match['3. type'] ?? '')),
|
'isin' => $isin,
|
||||||
'region' => trim((string) ($match['4. region'] ?? '')),
|
'type' => $type,
|
||||||
'market_open' => trim((string) ($match['5. marketOpen'] ?? '')),
|
'region' => $region,
|
||||||
'market_close' => trim((string) ($match['6. marketClose'] ?? '')),
|
'currency' => $currency,
|
||||||
'timezone' => trim((string) ($match['7. timezone'] ?? '')),
|
'match_score' => '',
|
||||||
'currency' => trim((string) ($match['8. currency'] ?? '')),
|
'raw' => [
|
||||||
'match_score' => trim((string) ($match['9. matchScore'] ?? '')),
|
'security' => $item,
|
||||||
|
'listing' => $listing,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
@@ -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 {
|
$mm->registerFunction($moduleName, 'bavest_fetch_chart_series', static function (string $isin): array {
|
||||||
$settings = modules()->settings('boersenchecker');
|
$isin = strtoupper(trim($isin));
|
||||||
$apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? ''));
|
if ($isin === '') {
|
||||||
$timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12);
|
return ['ok' => false, 'message' => 'Keine ISIN angegeben.'];
|
||||||
$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.'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheDir = sys_get_temp_dir() . '/boersenchecker-alpha-vantage';
|
$cacheDir = sys_get_temp_dir() . '/boersenchecker-bavest';
|
||||||
if (!is_dir($cacheDir)) {
|
if (!is_dir($cacheDir)) {
|
||||||
@mkdir($cacheDir, 0775, true);
|
@mkdir($cacheDir, 0775, true);
|
||||||
}
|
}
|
||||||
|
$cachePath = $cacheDir . '/' . md5('historical-price|' . $isin) . '.json';
|
||||||
|
|
||||||
$fetchPayload = static function (string $functionName, int $ttl) use ($symbol, $apiKey, $timeout, $cacheDir): array {
|
$decoded = null;
|
||||||
$cacheKey = md5($functionName . '|' . $symbol . '|' . $apiKey);
|
if (is_file($cachePath) && (time() - filemtime($cachePath)) < (6 * 3600)) {
|
||||||
$cachePath = $cacheDir . '/' . $cacheKey . '.json';
|
|
||||||
if (is_file($cachePath) && (time() - filemtime($cachePath)) < $ttl) {
|
|
||||||
$cached = file_get_contents($cachePath);
|
$cached = file_get_contents($cachePath);
|
||||||
$decoded = is_string($cached) ? json_decode($cached, true) : null;
|
$decoded = is_string($cached) ? json_decode($cached, true) : null;
|
||||||
if (is_array($decoded)) {
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = 'https://www.alphavantage.co/query?' . http_build_query([
|
if (!is_array($decoded)) {
|
||||||
'function' => $functionName,
|
$response = module_fn('boersenchecker', 'bavest_request', 'timeseries/history', [
|
||||||
'symbol' => $symbol,
|
'isin' => $isin,
|
||||||
'apikey' => $apiKey,
|
'from' => date('Y-m-d', strtotime('-6 years')),
|
||||||
'outputsize' => 'compact',
|
'to' => date('Y-m-d'),
|
||||||
]);
|
], 'application/json', 'GET');
|
||||||
|
if (empty($response['ok'])) {
|
||||||
$responseBody = null;
|
return $response;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
$decoded = is_array($response['data'] ?? null) ? $response['data'] : [];
|
||||||
|
@file_put_contents($cachePath, json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_string($responseBody) || $responseBody === '') {
|
$rows = [];
|
||||||
$context = stream_context_create([
|
if (isset($decoded['data']['prices']) && is_array($decoded['data']['prices'])) {
|
||||||
'http' => [
|
$rows = $decoded['data']['prices'];
|
||||||
'method' => 'GET',
|
} elseif (isset($decoded['data']) && is_array($decoded['data'])) {
|
||||||
'timeout' => $timeout,
|
$rows = $decoded['data'];
|
||||||
'header' => "Accept: application/json\r\n",
|
} elseif (isset($decoded['results']) && is_array($decoded['results'])) {
|
||||||
],
|
$rows = $decoded['results'];
|
||||||
]);
|
} elseif (array_is_list($decoded)) {
|
||||||
$responseBody = @file_get_contents($url, false, $context);
|
$rows = $decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
$decoded = is_string($responseBody) ? json_decode($responseBody, true) : null;
|
$dailyByDate = [];
|
||||||
if (is_array($decoded) && $decoded !== []) {
|
foreach ($rows as $row) {
|
||||||
@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)) {
|
if (!is_array($row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$close = $row['4. close'] ?? $row['5. adjusted close'] ?? null;
|
$date = trim((string) ($row['date'] ?? $row['time'] ?? $row['timestamp'] ?? ''));
|
||||||
if (!is_numeric($close)) {
|
$close = $row['close'] ?? $row['price'] ?? $row['c'] ?? null;
|
||||||
$close = $row['5. adjusted close'] ?? $row['4. close'] ?? null;
|
if ($date === '' || !is_numeric($close)) {
|
||||||
}
|
|
||||||
if (!is_numeric($close)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$points[] = [
|
$normalizedDate = date('Y-m-d', strtotime($date) ?: time());
|
||||||
'date' => (string) $date,
|
$dailyByDate[$normalizedDate] = [
|
||||||
|
'date' => date('Y-m-d', strtotime($date) ?: time()),
|
||||||
'close' => (float) $close,
|
'close' => (float) $close,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
$daily = array_values($dailyByDate);
|
||||||
|
|
||||||
usort($points, static fn (array $left, array $right): int => strcmp((string) $left['date'], (string) $right['date']));
|
usort($daily, static fn (array $left, array $right): int => strcmp((string) $left['date'], (string) $right['date']));
|
||||||
return $points;
|
if ($daily === []) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Keine historischen Schlusskurse fuer ' . $isin . ' verfuegbar.',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
$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);
|
||||||
};
|
};
|
||||||
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($dailyPayload['Note']) || !empty($weeklyPayload['Note']) || !empty($monthlyPayload['Note'])) {
|
|
||||||
return [
|
|
||||||
'ok' => false,
|
|
||||||
'message' => 'Alpha Vantage Limit erreicht. Bitte spaeter erneut versuchen.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$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.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'symbol' => $symbol,
|
'isin' => $isin,
|
||||||
'daily' => $daily,
|
'daily' => $daily,
|
||||||
'weekly' => $weekly,
|
'weekly' => $aggregate($daily, 'o-W'),
|
||||||
'monthly' => $monthly,
|
'monthly' => $aggregate($daily, 'Y-m'),
|
||||||
|
'source' => 'bavest:timeseries/history',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
{ "name": "db.password", "label": "DB Passwort", "type": "password", "required": false },
|
{ "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": "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": "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": "bavest_api_key", "label": "Bavest API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Bavest." },
|
||||||
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
|
{ "name": "bavest_timeout_sec", "label": "Bavest 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_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": {
|
"db_defaults": {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ $instrumentTable = module_fn('boersenchecker', 'table', 'instruments');
|
|||||||
$positionTable = module_fn('boersenchecker', 'table', 'positions');
|
$positionTable = module_fn('boersenchecker', 'table', 'positions');
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'SELECT i.id, i.name, i.symbol
|
'SELECT i.id, i.name, i.symbol, i.isin
|
||||||
FROM ' . $instrumentTable . ' i
|
FROM ' . $instrumentTable . ' i
|
||||||
INNER JOIN ' . $positionTable . ' p ON p.instrument_id = i.id
|
INNER JOIN ' . $positionTable . ' p ON p.instrument_id = i.id
|
||||||
WHERE i.id = :id AND p.owner_sub = :owner_sub
|
WHERE i.id = :id AND p.owner_sub = :owner_sub
|
||||||
@@ -37,12 +37,12 @@ if (!is_array($instrument)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$symbol = trim((string) ($instrument['symbol'] ?? ''));
|
$isin = strtoupper(trim((string) ($instrument['isin'] ?? '')));
|
||||||
if ($symbol === '') {
|
if ($isin === '') {
|
||||||
echo json_encode(['ok' => false, 'message' => 'Fuer diese Aktie ist kein Symbol hinterlegt.'], JSON_UNESCAPED_UNICODE);
|
echo json_encode(['ok' => false, 'message' => 'Fuer diese Aktie ist keine ISIN hinterlegt.'], JSON_UNESCAPED_UNICODE);
|
||||||
exit;
|
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);
|
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
angestossen und respektiert die dortige Max-Age-Logik.
|
angestossen und respektiert die dortige Max-Age-Logik.
|
||||||
</p>
|
</p>
|
||||||
<p class="muted" style="margin-top:12px;">
|
<p class="muted" style="margin-top:12px;">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
<div class="bc-actions" style="margin-top:16px;">
|
<div class="bc-actions" style="margin-top:16px;">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -90,13 +90,13 @@
|
|||||||
<button class="bc-button bc-button--primary" type="submit">FX-Daten aktualisieren</button>
|
<button class="bc-button bc-button--primary" type="submit">FX-Daten aktualisieren</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="action" value="refresh_alpha_vantage_all">
|
<input type="hidden" name="action" value="refresh_market_data_all">
|
||||||
<input type="hidden" name="owner_sub" value="<?= e((string) $ownerSub) ?>">
|
<input type="hidden" name="owner_sub" value="<?= e((string) $ownerSub) ?>">
|
||||||
<button class="bc-button bc-button--secondary" type="submit">Alle API-Kurse abrufen</button>
|
<button class="bc-button bc-button--secondary" type="submit">Alle API-Kurse abrufen</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin-top:12px;">
|
<div class="muted" style="margin-top:12px;">
|
||||||
Alpha Vantage Mindestabstand: <?= e((string) $alphaMinIntervalMinutes) ?> Min.
|
Bavest Mindestabstand: <?= e((string) $marketDataMinIntervalMinutes) ?> Min.
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin-top:6px;">
|
<div class="muted" style="margin-top:6px;">
|
||||||
API-Key und Timeout werden ueber <a href="/modules/setup/boersenchecker">Modul-Setup</a> gepflegt.
|
API-Key und Timeout werden ueber <a href="/modules/setup/boersenchecker">Modul-Setup</a> gepflegt.
|
||||||
@@ -198,8 +198,8 @@
|
|||||||
<section class="module-box-table">
|
<section class="module-box-table">
|
||||||
<div class="module-box-head">
|
<div class="module-box-head">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="module-box-title">API-Symbol suchen</h2>
|
<h2 class="module-box-title">Wertpapiersuche</h2>
|
||||||
<p>Alpha-Vantage-Symbole suchen und direkt ins Positionsformular uebernehmen.</p>
|
<p>Bavest-Suchergebnisse pruefen und Daten direkt ins Positionsformular uebernehmen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="module-box-copy">
|
<div class="module-box-copy">
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
<td><?= e((string) ($result['currency'] ?? '')) ?></td>
|
<td><?= e((string) ($result['currency'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($result['match_score'] ?? '')) ?></td>
|
<td><?= e((string) ($result['match_score'] ?? '')) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&symbol_candidate=<?= urlencode((string) ($result['symbol'] ?? '')) ?>&instrument_name_candidate=<?= urlencode((string) ($result['name'] ?? '')) ?>&market_candidate=<?= urlencode((string) ($result['region'] ?? '')) ?>"e_currency_candidate=<?= urlencode((string) ($result['currency'] ?? '')) ?>">
|
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&symbol_candidate=<?= urlencode((string) ($result['symbol'] ?? '')) ?>&instrument_name_candidate=<?= urlencode((string) ($result['name'] ?? '')) ?>&isin_candidate=<?= urlencode((string) ($result['isin'] ?? '')) ?>&market_candidate=<?= urlencode((string) ($result['region'] ?? '')) ?>"e_currency_candidate=<?= urlencode((string) ($result['currency'] ?? '')) ?>">
|
||||||
In Formular uebernehmen
|
In Formular uebernehmen
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&edit_position=<?= e((string) $position['id']) ?>">Bearbeiten</a>
|
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&edit_position=<?= e((string) $position['id']) ?>">Bearbeiten</a>
|
||||||
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&instrument_id=<?= e((string) $position['instrument_id']) ?>">Kurs erfassen</a>
|
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/depotverwaltung?owner_sub=<?= urlencode((string) $ownerSub) ?>&instrument_id=<?= e((string) $position['instrument_id']) ?>">Kurs erfassen</a>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="action" value="refresh_alpha_vantage_position">
|
<input type="hidden" name="action" value="refresh_market_data_position">
|
||||||
<input type="hidden" name="owner_sub" value="<?= e((string) $ownerSub) ?>">
|
<input type="hidden" name="owner_sub" value="<?= e((string) $ownerSub) ?>">
|
||||||
<input type="hidden" name="position_id" value="<?= e((string) $position['id']) ?>">
|
<input type="hidden" name="position_id" value="<?= e((string) $position['id']) ?>">
|
||||||
<button class="bc-button bc-button--secondary" type="submit">API-Kurs</button>
|
<button class="bc-button bc-button--secondary" type="submit">API-Kurs</button>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<input type="hidden" name="action" value="refresh_current_quotes_home">
|
<input type="hidden" name="action" value="refresh_current_quotes_home">
|
||||||
<input type="hidden" name="portfolio_id" value="<?= e((string) $selectedPortfolioId) ?>">
|
<input type="hidden" name="portfolio_id" value="<?= e((string) $selectedPortfolioId) ?>">
|
||||||
<div class="bc-field-label">Marktdaten</div>
|
<div class="bc-field-label">Marktdaten</div>
|
||||||
<p class="bc-text" style="margin-top:12px;">Aktuelle Kurse fuer das gewaehlte Depot ueber Alpha Vantage abrufen.</p>
|
<p class="bc-text" style="margin-top:12px;">Aktuelle Kurse fuer das gewaehlte Depot ueber Bavest per ISIN abrufen.</p>
|
||||||
<div class="bc-actions" style="margin-top:16px;">
|
<div class="bc-actions" style="margin-top:16px;">
|
||||||
<button class="bc-button bc-button--primary" type="submit" <?= $selectedPortfolioId > 0 ? '' : 'disabled' ?>>Aktuelle Kurse abrufen</button>
|
<button class="bc-button bc-button--primary" type="submit" <?= $selectedPortfolioId > 0 ? '' : 'disabled' ?>>Aktuelle Kurse abrufen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
<section class="module-box-table">
|
<section class="module-box-table">
|
||||||
<div class="module-box-head">
|
<div class="module-box-head">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="module-box-title">Symbolsuche</h2>
|
<h2 class="module-box-title">Wertpapiersuche</h2>
|
||||||
<p>Alpha-Vantage-Symbole finden und direkt fuer die Aktie uebernehmen.</p>
|
<p>Bavest-Suchergebnisse finden und direkt fuer die Aktie uebernehmen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="module-box-copy">
|
<div class="module-box-copy">
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<td><?= e((string) ($result['region'] ?? '')) ?></td>
|
<td><?= e((string) ($result['region'] ?? '')) ?></td>
|
||||||
<td><?= e((string) ($result['currency'] ?? '')) ?></td>
|
<td><?= e((string) ($result['currency'] ?? '')) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/aktienverwaltung?instrument_id=<?= e((string) $selectedInstrumentId) ?>&symbol_candidate=<?= urlencode((string) ($result['symbol'] ?? '')) ?>&instrument_name_candidate=<?= urlencode((string) ($result['name'] ?? '')) ?>&market_candidate=<?= urlencode((string) ($result['region'] ?? '')) ?>"e_currency_candidate=<?= urlencode((string) ($result['currency'] ?? '')) ?>">
|
<a class="bc-button bc-button--secondary" href="/module/boersenchecker/aktienverwaltung?instrument_id=<?= e((string) $selectedInstrumentId) ?>&symbol_candidate=<?= urlencode((string) ($result['symbol'] ?? '')) ?>&instrument_name_candidate=<?= urlencode((string) ($result['name'] ?? '')) ?>&isin_candidate=<?= urlencode((string) ($result['isin'] ?? '')) ?>&market_candidate=<?= urlencode((string) ($result['region'] ?? '')) ?>"e_currency_candidate=<?= urlencode((string) ($result['currency'] ?? '')) ?>">
|
||||||
Uebernehmen
|
Uebernehmen
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" style="margin-top:12px;">
|
<form method="post" style="margin-top:12px;">
|
||||||
<input type="hidden" name="action" value="refresh_alpha_vantage_instrument">
|
<input type="hidden" name="action" value="refresh_market_data_instrument">
|
||||||
<input type="hidden" name="instrument_id" value="<?= e((string) ($selectedInstrument['id'] ?? 0)) ?>">
|
<input type="hidden" name="instrument_id" value="<?= e((string) ($selectedInstrument['id'] ?? 0)) ?>">
|
||||||
<button class="bc-button bc-button--secondary" type="submit">Aktuellen API-Kurs abrufen</button>
|
<button class="bc-button bc-button--secondary" type="submit">Aktuellen API-Kurs abrufen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class DashboardPage
|
|||||||
private array $moduleSettings;
|
private array $moduleSettings;
|
||||||
private string $defaultReportCurrency;
|
private string $defaultReportCurrency;
|
||||||
private float $fxMaxAgeHours;
|
private float $fxMaxAgeHours;
|
||||||
private int $alphaMinIntervalMinutes;
|
private int $marketDataMinIntervalMinutes;
|
||||||
private int $editPortfolioId;
|
private int $editPortfolioId;
|
||||||
private int $editPositionId;
|
private int $editPositionId;
|
||||||
private string $portfolioTable;
|
private string $portfolioTable;
|
||||||
@@ -49,9 +49,9 @@ final class DashboardPage
|
|||||||
$this->fxMaxAgeHours = 6.0;
|
$this->fxMaxAgeHours = 6.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->alphaMinIntervalMinutes = (int) ($this->moduleSettings['alpha_vantage_min_interval_minutes'] ?? 60);
|
$this->marketDataMinIntervalMinutes = (int) ($this->moduleSettings['bavest_min_interval_minutes'] ?? 60);
|
||||||
if ($this->alphaMinIntervalMinutes <= 0) {
|
if ($this->marketDataMinIntervalMinutes <= 0) {
|
||||||
$this->alphaMinIntervalMinutes = 60;
|
$this->marketDataMinIntervalMinutes = 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->editPortfolioId = (int) ($_GET['edit_portfolio'] ?? 0);
|
$this->editPortfolioId = (int) ($_GET['edit_portfolio'] ?? 0);
|
||||||
@@ -100,7 +100,7 @@ final class DashboardPage
|
|||||||
'availableOwners' => array_values($this->availableOwners),
|
'availableOwners' => array_values($this->availableOwners),
|
||||||
'defaultReportCurrency' => $this->defaultReportCurrency,
|
'defaultReportCurrency' => $this->defaultReportCurrency,
|
||||||
'fxMaxAgeHours' => $this->fxMaxAgeHours,
|
'fxMaxAgeHours' => $this->fxMaxAgeHours,
|
||||||
'alphaMinIntervalMinutes' => $this->alphaMinIntervalMinutes,
|
'marketDataMinIntervalMinutes' => $this->marketDataMinIntervalMinutes,
|
||||||
'symbolSearchKeywords' => $this->symbolSearchKeywords,
|
'symbolSearchKeywords' => $this->symbolSearchKeywords,
|
||||||
'symbolSearchResults' => $this->symbolSearchResults,
|
'symbolSearchResults' => $this->symbolSearchResults,
|
||||||
'editPortfolio' => $state['editPortfolio'],
|
'editPortfolio' => $state['editPortfolio'],
|
||||||
@@ -127,8 +127,8 @@ final class DashboardPage
|
|||||||
'save_position' => $this->savePosition(),
|
'save_position' => $this->savePosition(),
|
||||||
'delete_position' => $this->deletePosition(),
|
'delete_position' => $this->deletePosition(),
|
||||||
'save_quote' => $this->saveQuote(),
|
'save_quote' => $this->saveQuote(),
|
||||||
'refresh_alpha_vantage_position' => $this->refreshAlphaVantagePosition(),
|
'refresh_market_data_position' => $this->refreshMarketDataPosition(),
|
||||||
'refresh_alpha_vantage_all' => $this->refreshAlphaVantageAll(),
|
'refresh_market_data_all' => $this->refreshMarketDataAll(),
|
||||||
'search_symbol' => $this->searchSymbol(),
|
'search_symbol' => $this->searchSymbol(),
|
||||||
'delete_quote' => $this->deleteQuote(),
|
'delete_quote' => $this->deleteQuote(),
|
||||||
'refresh_fx' => $this->refreshFx(),
|
'refresh_fx' => $this->refreshFx(),
|
||||||
@@ -172,6 +172,7 @@ final class DashboardPage
|
|||||||
|
|
||||||
$candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? ''));
|
$candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? ''));
|
||||||
$candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? ''));
|
$candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? ''));
|
||||||
|
$candidateIsin = trim((string) ($_GET['isin_candidate'] ?? ''));
|
||||||
$candidateMarket = trim((string) ($_GET['market_candidate'] ?? ''));
|
$candidateMarket = trim((string) ($_GET['market_candidate'] ?? ''));
|
||||||
$candidateCurrency = $this->normalizeCurrency((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency));
|
$candidateCurrency = $this->normalizeCurrency((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency));
|
||||||
|
|
||||||
@@ -179,6 +180,7 @@ final class DashboardPage
|
|||||||
$editPosition = [
|
$editPosition = [
|
||||||
'instrument_name' => $candidateName,
|
'instrument_name' => $candidateName,
|
||||||
'symbol' => $candidateSymbol,
|
'symbol' => $candidateSymbol,
|
||||||
|
'isin' => $candidateIsin,
|
||||||
'market' => $candidateMarket,
|
'market' => $candidateMarket,
|
||||||
'quote_currency' => $candidateCurrency,
|
'quote_currency' => $candidateCurrency,
|
||||||
'purchase_currency' => $this->defaultReportCurrency,
|
'purchase_currency' => $this->defaultReportCurrency,
|
||||||
@@ -191,6 +193,9 @@ final class DashboardPage
|
|||||||
if ($candidateSymbol !== '') {
|
if ($candidateSymbol !== '') {
|
||||||
$editPosition['symbol'] = $candidateSymbol;
|
$editPosition['symbol'] = $candidateSymbol;
|
||||||
}
|
}
|
||||||
|
if ($candidateIsin !== '') {
|
||||||
|
$editPosition['isin'] = $candidateIsin;
|
||||||
|
}
|
||||||
if ($candidateMarket !== '') {
|
if ($candidateMarket !== '') {
|
||||||
$editPosition['market'] = $candidateMarket;
|
$editPosition['market'] = $candidateMarket;
|
||||||
}
|
}
|
||||||
@@ -378,7 +383,7 @@ final class DashboardPage
|
|||||||
return 'Kurs gespeichert.';
|
return 'Kurs gespeichert.';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function refreshAlphaVantagePosition(): string
|
private function refreshMarketDataPosition(): string
|
||||||
{
|
{
|
||||||
$positionId = (int) ($_POST['position_id'] ?? 0);
|
$positionId = (int) ($_POST['position_id'] ?? 0);
|
||||||
$stmt = $this->pdo->prepare(
|
$stmt = $this->pdo->prepare(
|
||||||
@@ -386,6 +391,7 @@ final class DashboardPage
|
|||||||
p.instrument_id,
|
p.instrument_id,
|
||||||
i.name AS instrument_name,
|
i.name AS instrument_name,
|
||||||
i.symbol,
|
i.symbol,
|
||||||
|
i.isin,
|
||||||
i.quote_currency
|
i.quote_currency
|
||||||
FROM ' . $this->positionTable . ' p
|
FROM ' . $this->positionTable . ' p
|
||||||
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
||||||
@@ -402,19 +408,19 @@ final class DashboardPage
|
|||||||
}
|
}
|
||||||
|
|
||||||
$instrumentId = (int) $row['instrument_id'];
|
$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));
|
$quoteCurrency = $this->normalizeCurrency((string) ($row['quote_currency'] ?? $this->defaultReportCurrency));
|
||||||
if ($symbol === '') {
|
if ($isin === '') {
|
||||||
throw new RuntimeException('Fuer diese Aktie ist noch kein API-Symbol / Ticker hinterlegt.');
|
throw new RuntimeException('Fuer diese Aktie ist noch keine ISIN hinterlegt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
||||||
$latestTimestamp = is_array($latestApiQuote) ? strtotime((string) ($latestApiQuote['quoted_at'] ?? '')) : false;
|
$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)) {
|
||||||
return 'Vorhandener Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' wiederverwendet.';
|
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'])) {
|
if (empty($apiResult['ok'])) {
|
||||||
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
|
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
|
||||||
}
|
}
|
||||||
@@ -426,16 +432,17 @@ final class DashboardPage
|
|||||||
(string) $apiResult['fetched_at'],
|
(string) $apiResult['fetched_at'],
|
||||||
(string) $apiResult['source']
|
(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(
|
$stmt = $this->pdo->prepare(
|
||||||
'SELECT DISTINCT
|
'SELECT DISTINCT
|
||||||
i.id,
|
i.id,
|
||||||
i.name,
|
i.name,
|
||||||
i.symbol,
|
i.symbol,
|
||||||
|
i.isin,
|
||||||
i.quote_currency
|
i.quote_currency
|
||||||
FROM ' . $this->positionTable . ' p
|
FROM ' . $this->positionTable . ' p
|
||||||
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
||||||
@@ -454,30 +461,39 @@ final class DashboardPage
|
|||||||
$failed = 0;
|
$failed = 0;
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
|
$bulkRows = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$instrumentId = (int) ($row['id'] ?? 0);
|
$instrumentId = (int) ($row['id'] ?? 0);
|
||||||
$symbol = strtoupper(trim((string) ($row['symbol'] ?? '')));
|
$isin = strtoupper(trim((string) ($row['isin'] ?? '')));
|
||||||
$quoteCurrency = $this->normalizeCurrency((string) ($row['quote_currency'] ?? $this->defaultReportCurrency));
|
if ($instrumentId <= 0 || $isin === '') {
|
||||||
|
|
||||||
if ($instrumentId <= 0 || $symbol === '') {
|
|
||||||
$skipped++;
|
$skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
||||||
$latestTimestamp = is_array($latestApiQuote) ? strtotime((string) ($latestApiQuote['quoted_at'] ?? '')) : false;
|
$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++;
|
$reused++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol);
|
$bulkRows[] = $row;
|
||||||
if (empty($apiResult['ok'])) {
|
|
||||||
$failed++;
|
|
||||||
$errors[] = (string) ($row['name'] ?? $symbol) . ': ' . (string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.');
|
|
||||||
if (stripos((string) ($apiResult['message'] ?? ''), 'limit') !== false) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,22 +506,23 @@ final class DashboardPage
|
|||||||
);
|
);
|
||||||
$fetched++;
|
$fetched++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($errors !== []) {
|
if ($errors !== []) {
|
||||||
throw new RuntimeException(
|
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))
|
. 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
|
private function searchSymbol(): string
|
||||||
{
|
{
|
||||||
$keywords = trim((string) ($_POST['search_keywords'] ?? ''));
|
$keywords = trim((string) ($_POST['search_keywords'] ?? ''));
|
||||||
$this->symbolSearchKeywords = $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'] : [];
|
$this->symbolSearchResults = is_array($result['results'] ?? null) ? $result['results'] : [];
|
||||||
|
|
||||||
if (empty($result['ok'])) {
|
if (empty($result['ok'])) {
|
||||||
@@ -708,7 +725,7 @@ final class DashboardPage
|
|||||||
);
|
);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'instrument_id' => $instrumentId,
|
'instrument_id' => $instrumentId,
|
||||||
'source' => 'alpha_vantage:%',
|
'source' => 'bavest:%',
|
||||||
]);
|
]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
return is_array($row) ? $row : null;
|
return is_array($row) ? $row : null;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class HomePage
|
|||||||
private string $positionTable;
|
private string $positionTable;
|
||||||
private string $quoteTable;
|
private string $quoteTable;
|
||||||
private string $defaultReportCurrency;
|
private string $defaultReportCurrency;
|
||||||
private int $alphaMinIntervalMinutes;
|
private int $marketDataMinIntervalMinutes;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -26,9 +26,9 @@ final class HomePage
|
|||||||
|
|
||||||
$settings = \modules()->settings('boersenchecker');
|
$settings = \modules()->settings('boersenchecker');
|
||||||
$this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR';
|
$this->defaultReportCurrency = strtoupper(trim((string) ($settings['report_currency'] ?? 'EUR'))) ?: 'EUR';
|
||||||
$this->alphaMinIntervalMinutes = (int) ($settings['alpha_vantage_min_interval_minutes'] ?? 60);
|
$this->marketDataMinIntervalMinutes = (int) ($settings['bavest_min_interval_minutes'] ?? 60);
|
||||||
if ($this->alphaMinIntervalMinutes <= 0) {
|
if ($this->marketDataMinIntervalMinutes <= 0) {
|
||||||
$this->alphaMinIntervalMinutes = 60;
|
$this->marketDataMinIntervalMinutes = 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
$table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name);
|
$table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name);
|
||||||
@@ -118,7 +118,7 @@ final class HomePage
|
|||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare(
|
$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
|
FROM ' . $this->positionTable . ' p
|
||||||
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
INNER JOIN ' . $this->instrumentTable . ' i ON i.id = p.instrument_id
|
||||||
WHERE p.owner_sub = :owner_sub AND p.portfolio_id = :portfolio_id'
|
WHERE p.owner_sub = :owner_sub AND p.portfolio_id = :portfolio_id'
|
||||||
@@ -134,38 +134,48 @@ final class HomePage
|
|||||||
|
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
$reused = 0;
|
$reused = 0;
|
||||||
|
$bulkCandidates = [];
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
$instrumentId = (int) ($row['id'] ?? 0);
|
$instrumentId = (int) ($row['id'] ?? 0);
|
||||||
$symbol = strtoupper(trim((string) ($row['symbol'] ?? '')));
|
$isin = strtoupper(trim((string) ($row['isin'] ?? '')));
|
||||||
if ($instrumentId <= 0 || $symbol === '') {
|
if ($instrumentId <= 0 || $isin === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$latest = $this->latestApiQuoteForInstrument($instrumentId);
|
$latest = $this->latestApiQuoteForInstrument($instrumentId);
|
||||||
$latestTimestamp = is_array($latest) ? strtotime((string) ($latest['quoted_at'] ?? '')) : false;
|
$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++;
|
$reused++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quote', $symbol);
|
$bulkCandidates[] = $row;
|
||||||
if (empty($apiResult['ok'])) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($bulkCandidates !== []) {
|
||||||
|
$bulkResult = \module_fn('boersenchecker', 'bavest_fetch_bulk_quotes', $bulkCandidates);
|
||||||
|
$quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : [];
|
||||||
$stmtInsert = $this->pdo->prepare(
|
$stmtInsert = $this->pdo->prepare(
|
||||||
'INSERT INTO ' . $this->quoteTable . ' (instrument_id, price, currency, quoted_at, source)
|
'INSERT INTO ' . $this->quoteTable . ' (instrument_id, price, currency, quoted_at, source)
|
||||||
VALUES (:instrument_id, :price, :currency, :quoted_at, :source)'
|
VALUES (:instrument_id, :price, :currency, :quoted_at, :source)'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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([
|
$stmtInsert->execute([
|
||||||
'instrument_id' => $instrumentId,
|
'instrument_id' => $instrumentId,
|
||||||
'price' => (float) $apiResult['price'],
|
'price' => (float) $quote['price'],
|
||||||
'currency' => strtoupper(trim((string) ($row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
|
'currency' => strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
|
||||||
'quoted_at' => (string) $apiResult['fetched_at'],
|
'quoted_at' => (string) ($quote['fetched_at'] ?? date('Y-m-d H:i:s')),
|
||||||
'source' => (string) $apiResult['source'],
|
'source' => (string) ($quote['source'] ?? 'bavest:quote'),
|
||||||
]);
|
]);
|
||||||
$updated++;
|
$updated++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 'Aktuelle Kurse: ' . $updated . ' aktualisiert, ' . $reused . ' wiederverwendet.';
|
return 'Aktuelle Kurse: ' . $updated . ' aktualisiert, ' . $reused . ' wiederverwendet.';
|
||||||
}
|
}
|
||||||
@@ -241,7 +251,7 @@ final class HomePage
|
|||||||
);
|
);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'instrument_id' => $instrumentId,
|
'instrument_id' => $instrumentId,
|
||||||
'source' => 'alpha_vantage:%',
|
'source' => 'bavest:%',
|
||||||
]);
|
]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
return is_array($row) ? $row : null;
|
return is_array($row) ? $row : null;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ final class InstrumentPage
|
|||||||
|
|
||||||
$candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? ''));
|
$candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? ''));
|
||||||
$candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? ''));
|
$candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? ''));
|
||||||
|
$candidateIsin = trim((string) ($_GET['isin_candidate'] ?? ''));
|
||||||
$candidateMarket = trim((string) ($_GET['market_candidate'] ?? ''));
|
$candidateMarket = trim((string) ($_GET['market_candidate'] ?? ''));
|
||||||
$candidateCurrency = strtoupper(trim((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency;
|
$candidateCurrency = strtoupper(trim((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency;
|
||||||
if ($selectedInstrument === null && ($candidateName !== '' || $candidateSymbol !== '' || $candidateMarket !== '')) {
|
if ($selectedInstrument === null && ($candidateName !== '' || $candidateSymbol !== '' || $candidateMarket !== '')) {
|
||||||
@@ -79,9 +80,9 @@ final class InstrumentPage
|
|||||||
'id' => 0,
|
'id' => 0,
|
||||||
'name' => $candidateName,
|
'name' => $candidateName,
|
||||||
'symbol' => $candidateSymbol,
|
'symbol' => $candidateSymbol,
|
||||||
|
'isin' => $candidateIsin,
|
||||||
'market' => $candidateMarket,
|
'market' => $candidateMarket,
|
||||||
'quote_currency' => $candidateCurrency,
|
'quote_currency' => $candidateCurrency,
|
||||||
'isin' => '',
|
|
||||||
'wkn' => '',
|
'wkn' => '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,7 @@ final class InstrumentPage
|
|||||||
'save_instrument' => $this->saveInstrument(),
|
'save_instrument' => $this->saveInstrument(),
|
||||||
'save_quote' => $this->saveQuote(),
|
'save_quote' => $this->saveQuote(),
|
||||||
'delete_quote' => $this->deleteQuote(),
|
'delete_quote' => $this->deleteQuote(),
|
||||||
'refresh_alpha_vantage_instrument' => $this->refreshInstrumentQuote(),
|
'refresh_market_data_instrument' => $this->refreshInstrumentQuote(),
|
||||||
'search_symbol' => $this->searchSymbol(),
|
'search_symbol' => $this->searchSymbol(),
|
||||||
default => '',
|
default => '',
|
||||||
};
|
};
|
||||||
@@ -211,12 +212,12 @@ final class InstrumentPage
|
|||||||
{
|
{
|
||||||
$instrumentId = (int) ($_POST['instrument_id'] ?? 0);
|
$instrumentId = (int) ($_POST['instrument_id'] ?? 0);
|
||||||
$instrument = $this->assertInstrumentAccessible($instrumentId);
|
$instrument = $this->assertInstrumentAccessible($instrumentId);
|
||||||
$symbol = trim((string) ($instrument['symbol'] ?? ''));
|
$isin = strtoupper(trim((string) ($instrument['isin'] ?? '')));
|
||||||
if ($symbol === '') {
|
if ($isin === '') {
|
||||||
throw new RuntimeException('Fuer diese Aktie ist kein Symbol hinterlegt.');
|
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'])) {
|
if (empty($apiResult['ok'])) {
|
||||||
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
|
throw new RuntimeException((string) ($apiResult['message'] ?? 'API-Abruf fehlgeschlagen.'));
|
||||||
}
|
}
|
||||||
@@ -233,13 +234,13 @@ final class InstrumentPage
|
|||||||
'source' => (string) $apiResult['source'],
|
'source' => (string) $apiResult['source'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return 'API-Kurs gespeichert.';
|
return 'Bavest-Kurs gespeichert.';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function searchSymbol(): string
|
private function searchSymbol(): string
|
||||||
{
|
{
|
||||||
$this->searchKeywords = trim((string) ($_POST['search_keywords'] ?? ''));
|
$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'] : [];
|
$this->searchResults = is_array($result['results'] ?? null) ? $result['results'] : [];
|
||||||
if (empty($result['ok'])) {
|
if (empty($result['ok'])) {
|
||||||
throw new RuntimeException((string) ($result['message'] ?? 'Symbolsuche fehlgeschlagen.'));
|
throw new RuntimeException((string) ($result['message'] ?? 'Symbolsuche fehlgeschlagen.'));
|
||||||
|
|||||||
Reference in New Issue
Block a user