umstellung
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-24 02:00:37 +02:00
parent c2c91032db
commit 9103218c35
10 changed files with 449 additions and 399 deletions

View File

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

View File

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

View File

@@ -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.'));