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

This commit is contained in:
2026-04-22 01:04:43 +02:00
parent 04a4c3f2c1
commit a1bab34bd3
3 changed files with 234 additions and 0 deletions

View File

@@ -400,3 +400,139 @@ $mm->registerFunction($moduleName, 'alpha_vantage_fetch_quote', static function
'raw' => $quote, 'raw' => $quote,
]; ];
}); });
$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);
if ($keywords === '') {
return [
'ok' => false,
'message' => 'Bitte Suchbegriff angeben.',
'results' => [],
];
}
if ($apiKey === '') {
return [
'ok' => false,
'message' => 'Alpha-Vantage-API-Key fehlt. Bitte im Modul-Setup hinterlegen.',
'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' => [],
];
}
}
}
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)) {
continue;
}
$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'] ?? '')),
];
}
return [
'ok' => true,
'message' => count($results) . ' Treffer gefunden.',
'results' => $results,
];
});

View File

@@ -156,6 +156,56 @@
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="card" style="margin-top:1rem; background:var(--panel-2);">
<strong>API-Symbol suchen</strong>
<p class="muted" style="margin-top:.75rem;">
Suche nach Firma oder Ticker ueber den Alpha-Vantage-`SYMBOL_SEARCH`-Endpoint und uebernimm einen Treffer direkt ins Positionsformular.
</p>
<form method="post" style="margin-top:.75rem; display:flex; gap:10px; flex-wrap:wrap; align-items:end;">
<input type="hidden" name="action" value="search_symbol">
<label class="setup-field muted" style="margin:0; min-width:260px; flex:1;">
<span>Suchbegriff</span>
<input type="text" name="search_keywords" value="<?= e($symbolSearchKeywords) ?>" placeholder="z.B. Mercedes, AAPL, Allianz" required>
</label>
<button class="cta-button" type="submit">Suchen</button>
</form>
<?php if ($symbolSearchResults !== []): ?>
<div style="overflow:auto; margin-top:1rem;">
<table style="width:100%; border-collapse:collapse;">
<thead>
<tr style="text-align:left; border-bottom:1px solid var(--border);">
<th style="padding:8px;">Symbol</th>
<th style="padding:8px;">Name</th>
<th style="padding:8px;">Typ</th>
<th style="padding:8px;">Region</th>
<th style="padding:8px;">Waehrung</th>
<th style="padding:8px;">Match</th>
<th style="padding:8px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($symbolSearchResults as $result): ?>
<tr style="border-bottom:1px solid var(--border); vertical-align:top;">
<td style="padding:8px;"><strong><?= e((string) ($result['symbol'] ?? '')) ?></strong></td>
<td style="padding:8px;"><?= e((string) ($result['name'] ?? '')) ?></td>
<td style="padding:8px;"><?= e((string) ($result['type'] ?? '')) ?></td>
<td style="padding:8px;"><?= e((string) ($result['region'] ?? '')) ?></td>
<td style="padding:8px;"><?= e((string) ($result['currency'] ?? '')) ?></td>
<td style="padding:8px;"><?= e((string) ($result['match_score'] ?? '')) ?></td>
<td style="padding:8px;">
<a class="nav-link" href="/module/boersenchecker?symbol_candidate=<?= urlencode((string) ($result['symbol'] ?? '')) ?>&instrument_name_candidate=<?= urlencode((string) ($result['name'] ?? '')) ?>&market_candidate=<?= urlencode((string) ($result['region'] ?? '')) ?>&quote_currency_candidate=<?= urlencode((string) ($result['currency'] ?? '')) ?>">
In Formular uebernehmen
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="card" style="margin-top:1rem; background:var(--panel-2);"> <div class="card" style="margin-top:1rem; background:var(--panel-2);">
<strong>Manuellen Kurs erfassen</strong> <strong>Manuellen Kurs erfassen</strong>
<?php if ($instrumentList === []): ?> <?php if ($instrumentList === []): ?>

View File

@@ -21,6 +21,8 @@ final class DashboardPage
private string $instrumentTable; private string $instrumentTable;
private string $positionTable; private string $positionTable;
private string $quoteTable; private string $quoteTable;
private string $symbolSearchKeywords = '';
private array $symbolSearchResults = [];
public function __construct() public function __construct()
{ {
@@ -79,6 +81,8 @@ final class DashboardPage
'defaultReportCurrency' => $this->defaultReportCurrency, 'defaultReportCurrency' => $this->defaultReportCurrency,
'fxMaxAgeHours' => $this->fxMaxAgeHours, 'fxMaxAgeHours' => $this->fxMaxAgeHours,
'alphaMinIntervalMinutes' => $this->alphaMinIntervalMinutes, 'alphaMinIntervalMinutes' => $this->alphaMinIntervalMinutes,
'symbolSearchKeywords' => $this->symbolSearchKeywords,
'symbolSearchResults' => $this->symbolSearchResults,
'editPortfolio' => $state['editPortfolio'], 'editPortfolio' => $state['editPortfolio'],
'editPosition' => $state['editPosition'], 'editPosition' => $state['editPosition'],
'portfolios' => $state['portfolios'], 'portfolios' => $state['portfolios'],
@@ -105,6 +109,7 @@ final class DashboardPage
'save_quote' => $this->saveQuote(), 'save_quote' => $this->saveQuote(),
'refresh_alpha_vantage_position' => $this->refreshAlphaVantagePosition(), 'refresh_alpha_vantage_position' => $this->refreshAlphaVantagePosition(),
'refresh_alpha_vantage_all' => $this->refreshAlphaVantageAll(), 'refresh_alpha_vantage_all' => $this->refreshAlphaVantageAll(),
'search_symbol' => $this->searchSymbol(),
'delete_quote' => $this->deleteQuote(), 'delete_quote' => $this->deleteQuote(),
'refresh_fx' => $this->refreshFx(), 'refresh_fx' => $this->refreshFx(),
default => '', default => '',
@@ -145,6 +150,35 @@ final class DashboardPage
); );
} }
$candidateName = trim((string) ($_GET['instrument_name_candidate'] ?? ''));
$candidateSymbol = trim((string) ($_GET['symbol_candidate'] ?? ''));
$candidateMarket = trim((string) ($_GET['market_candidate'] ?? ''));
$candidateCurrency = $this->normalizeCurrency((string) ($_GET['quote_currency_candidate'] ?? $this->defaultReportCurrency));
if ($editPosition === null) {
$editPosition = [
'instrument_name' => $candidateName,
'symbol' => $candidateSymbol,
'market' => $candidateMarket,
'quote_currency' => $candidateCurrency,
'purchase_currency' => $this->defaultReportCurrency,
'purchase_date' => date('Y-m-d'),
];
} else {
if ($candidateName !== '') {
$editPosition['instrument_name'] = $candidateName;
}
if ($candidateSymbol !== '') {
$editPosition['symbol'] = $candidateSymbol;
}
if ($candidateMarket !== '') {
$editPosition['market'] = $candidateMarket;
}
if ($candidateCurrency !== '') {
$editPosition['quote_currency'] = $candidateCurrency;
}
}
return [ return [
'portfolios' => $portfolios, 'portfolios' => $portfolios,
'portfolioById' => $portfolioById, 'portfolioById' => $portfolioById,
@@ -447,6 +481,20 @@ final class DashboardPage
return 'Alpha Vantage: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne Symbol, ' . $failed . ' Fehler.'; return 'Alpha Vantage: ' . $fetched . ' neu, ' . $reused . ' wiederverwendet, ' . $skipped . ' ohne Symbol, ' . $failed . ' Fehler.';
} }
private function searchSymbol(): string
{
$keywords = trim((string) ($_POST['search_keywords'] ?? ''));
$this->symbolSearchKeywords = $keywords;
$result = \module_fn('boersenchecker', 'alpha_vantage_search_symbols', $keywords);
$this->symbolSearchResults = is_array($result['results'] ?? null) ? $result['results'] : [];
if (empty($result['ok'])) {
throw new RuntimeException((string) ($result['message'] ?? 'Symbolsuche fehlgeschlagen.'));
}
return (string) ($result['message'] ?? 'Suche abgeschlossen.');
}
private function deleteQuote(): string private function deleteQuote(): string
{ {
$quoteId = (int) ($_POST['quote_id'] ?? 0); $quoteId = (int) ($_POST['quote_id'] ?? 0);