dsfd
This commit is contained in:
@@ -3,18 +3,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use App\ModuleConfigException;
|
use App\ModuleConfigException;
|
||||||
|
|
||||||
spl_autoload_register(static function (string $class): void {
|
|
||||||
$prefix = 'Modules\\MiningChecker\\';
|
|
||||||
if (!str_starts_with($class, $prefix)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$file = dirname(__DIR__) . '/mining-checker/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
|
|
||||||
if (is_file($file)) {
|
|
||||||
require_once $file;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
spl_autoload_register(static function (string $class): void {
|
spl_autoload_register(static function (string $class): void {
|
||||||
$prefix = 'Modules\\Boersenchecker\\';
|
$prefix = 'Modules\\Boersenchecker\\';
|
||||||
if (!str_starts_with($class, $prefix)) {
|
if (!str_starts_with($class, $prefix)) {
|
||||||
@@ -228,34 +216,11 @@ $mm->registerFunction($moduleName, 'fx_service', static function (): ?object {
|
|||||||
try {
|
try {
|
||||||
return module_fn('fx-rates', 'service');
|
return module_fn('fx-rates', 'service');
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_dir(dirname(__DIR__) . '/mining-checker')) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$config = \Modules\MiningChecker\Infrastructure\ModuleConfig::load(dirname(__DIR__) . '/mining-checker');
|
|
||||||
$repo = new \Modules\MiningChecker\Infrastructure\MiningRepository(
|
|
||||||
\Modules\MiningChecker\Infrastructure\ConnectionFactory::make($config),
|
|
||||||
$config->tablePrefix()
|
|
||||||
);
|
|
||||||
$fx = $config->fx();
|
|
||||||
|
|
||||||
return new \Modules\MiningChecker\Domain\FxService(
|
|
||||||
$repo,
|
|
||||||
(string) ($fx['url'] ?? 'https://currencyapi.net'),
|
|
||||||
(string) ($fx['currencies_url'] ?? ($fx['url'] ?? 'https://currencyapi.net')),
|
|
||||||
(int) ($fx['timeout'] ?? 10),
|
|
||||||
(int) ($fx['cache_ttl'] ?? 21600),
|
|
||||||
false,
|
|
||||||
(string) ($fx['provider'] ?? 'currencyapi'),
|
|
||||||
(string) ($fx['api_key'] ?? '')
|
|
||||||
);
|
|
||||||
} catch (\Throwable) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
$mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCurrency = 'EUR', float $maxAgeHours = 6.0): array {
|
$mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCurrency = 'EUR', float $maxAgeHours = 6.0): array {
|
||||||
@@ -263,7 +228,7 @@ $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCu
|
|||||||
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
|
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
|
||||||
return [
|
return [
|
||||||
'ok' => false,
|
'ok' => false,
|
||||||
'message' => 'FX-Service ist aktuell nicht verfuegbar.',
|
'message' => 'Das Modul fx-rates ist aktuell nicht verfuegbar oder nicht aktiviert.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +249,119 @@ $mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCu
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'fx_prepare_fetch', static function (
|
||||||
|
string $baseCurrency = 'EUR',
|
||||||
|
array $currencies = [],
|
||||||
|
float $maxAgeHours = 6.0
|
||||||
|
): array {
|
||||||
|
$service = module_fn('boersenchecker', 'fx_service');
|
||||||
|
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'Das Modul fx-rates ist aktuell nicht verfuegbar oder nicht aktiviert.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseCurrency = strtoupper(trim($baseCurrency)) ?: 'EUR';
|
||||||
|
$currencies = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (mixed $code): string => strtoupper(trim((string) $code)),
|
||||||
|
$currencies
|
||||||
|
), static fn (string $code): bool => $code !== '')));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $service->ensureFreshLatestRates($maxAgeHours, $baseCurrency, $currencies);
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'message' => !empty($result['reused']) ? 'Vorhandene FX-Daten weiterverwendet.' : 'FX-Daten aktualisiert.',
|
||||||
|
'result' => $result,
|
||||||
|
'fetch_id' => is_numeric($result['fetch_id'] ?? null) ? (int) $result['fetch_id'] : null,
|
||||||
|
'reused' => !empty($result['reused']),
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'message' => 'FX-Aktualisierung fehlgeschlagen: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'fx_source_with_fetch_id', static function (string $source, ?int $fetchId = null): string {
|
||||||
|
$source = trim($source) !== '' ? trim($source) : 'manual';
|
||||||
|
if ($fetchId === null || $fetchId <= 0) {
|
||||||
|
return preg_replace('/\|fx_fetch:\d+$/', '', $source) ?: $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = preg_replace('/\|fx_fetch:\d+$/', '', $source) ?: $source;
|
||||||
|
return $source . '|fx_fetch:' . $fetchId;
|
||||||
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'fx_extract_fetch_id', static function (?string $source): ?int {
|
||||||
|
$source = trim((string) $source);
|
||||||
|
if ($source === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\|fx_fetch:(\d+)$/', $source, $matches) === 1) {
|
||||||
|
$fetchId = (int) ($matches[1] ?? 0);
|
||||||
|
return $fetchId > 0 ? $fetchId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'fx_convert_with_fetch', static function (
|
||||||
|
?float $amount,
|
||||||
|
?string $fromCurrency,
|
||||||
|
?string $toCurrency,
|
||||||
|
?int $fetchId = null
|
||||||
|
): ?float {
|
||||||
|
if ($amount === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$from = strtoupper(trim((string) $fromCurrency));
|
||||||
|
$to = strtoupper(trim((string) $toCurrency));
|
||||||
|
if ($from === '' || $to === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($from === $to) {
|
||||||
|
return $amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = module_fn('boersenchecker', 'fx_service');
|
||||||
|
if (!$service) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedFetchId = $fetchId !== null && $fetchId > 0 ? $fetchId : null;
|
||||||
|
if ($normalizedFetchId !== null && method_exists($service, 'snapshotByFetchId')) {
|
||||||
|
try {
|
||||||
|
$snapshot = $service->snapshotByFetchId($normalizedFetchId, null, [$from, $to]);
|
||||||
|
if (is_array($snapshot)) {
|
||||||
|
$base = strtoupper(trim((string) ($snapshot['base_currency'] ?? '')));
|
||||||
|
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
|
||||||
|
$fromRate = $from === $base ? 1.0 : (is_numeric($rates[$from] ?? null) ? (float) $rates[$from] : null);
|
||||||
|
$toRate = $to === $base ? 1.0 : (is_numeric($rates[$to] ?? null) ? (float) $rates[$to] : null);
|
||||||
|
if ($fromRate !== null && $fromRate > 0 && $toRate !== null && $toRate > 0) {
|
||||||
|
return $amount * ($toRate / $fromRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!method_exists($service, 'convert')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = $service->convert($amount, $from, $to);
|
||||||
|
return is_numeric($value) ? (float) $value : null;
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$mm->registerFunction($moduleName, 'alpha_vantage_request', static function (
|
$mm->registerFunction($moduleName, 'alpha_vantage_request', static function (
|
||||||
string $functionName,
|
string $functionName,
|
||||||
array $params = []
|
array $params = []
|
||||||
@@ -648,6 +726,16 @@ $mm->registerFunction($moduleName, 'scheduled_refresh_quotes', static function (
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (array $row): string => strtoupper(trim((string) ($row['quote_currency'] ?? ''))),
|
||||||
|
$candidates
|
||||||
|
), static fn (string $code): bool => $code !== '')));
|
||||||
|
$fxResult = module_fn('boersenchecker', 'fx_prepare_fetch', $defaultReportCurrency, $quoteCurrencies, (float) (($settings['fx_max_age_hours'] ?? null) ?: 6));
|
||||||
|
if (empty($fxResult['ok'])) {
|
||||||
|
return $fxResult;
|
||||||
|
}
|
||||||
|
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
|
||||||
|
|
||||||
$bulkResult = module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $candidates);
|
$bulkResult = module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $candidates);
|
||||||
if (empty($bulkResult['ok'])) {
|
if (empty($bulkResult['ok'])) {
|
||||||
return [
|
return [
|
||||||
@@ -673,7 +761,7 @@ $mm->registerFunction($moduleName, 'scheduled_refresh_quotes', static function (
|
|||||||
(float) $quote['price'],
|
(float) $quote['price'],
|
||||||
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $defaultReportCurrency))) ?: $defaultReportCurrency,
|
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $defaultReportCurrency))) ?: $defaultReportCurrency,
|
||||||
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
|
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
|
||||||
(string) ($quote['source'] ?? 'alphavantage:global_quote')
|
(string) module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) ($quote['source'] ?? 'alphavantage:global_quote'), $fxFetchId)
|
||||||
);
|
);
|
||||||
if (!empty($storeResult['inserted'])) {
|
if (!empty($storeResult['inserted'])) {
|
||||||
$updated++;
|
$updated++;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{ "name": "db.user", "label": "DB User", "type": "text", "required": false },
|
{ "name": "db.user", "label": "DB User", "type": "text", "required": false },
|
||||||
{ "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 das Modul fx-rates genutzt." },
|
||||||
{ "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." },
|
{ "name": "alpha_vantage_api_key", "label": "Alpha Vantage API Key", "type": "password", "required": false, "help": "API Key fuer Aktienkursabrufe und Suche ueber Alpha Vantage." },
|
||||||
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
|
{ "name": "alpha_vantage_timeout_sec", "label": "Alpha Vantage Timeout (Sek.)", "type": "number", "required": false, "help": "HTTP-Timeout fuer API-Abrufe." },
|
||||||
{ "name": "alpha_vantage_min_interval_minutes", "label": "Alpha Vantage Mindestabstand (Min.)", "type": "number", "required": false, "help": "Wenn bereits ein frischer Alpha-Vantage-Kurs existiert, wird dieser wiederverwendet statt erneut abzurufen." },
|
{ "name": "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." },
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted" style="margin-top:16px;">
|
<p class="muted" style="margin-top:16px;">
|
||||||
Die Umrechnung liest gespeicherte FX-Daten aus dem Mining-Checker. Eine Aktualisierung wird nur manuell
|
Die Umrechnung liest gespeicherte FX-Daten zentral aus dem Modul fx-rates. Eine Aktualisierung wird nur manuell
|
||||||
angestossen und respektiert die dortige Max-Age-Logik.
|
angestossen und respektiert die dortige Max-Age- und Reuse-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 Symbols abgerufen. Die ISIN bleibt als Stammdatum erhalten.
|
Aktienkurse werden ueber Alpha Vantage anhand des hinterlegten Symbols abgerufen. Die ISIN bleibt als Stammdatum erhalten.
|
||||||
@@ -99,7 +99,10 @@
|
|||||||
Alpha Vantage Mindestabstand: <?= e((string) $marketDataMinIntervalMinutes) ?> Min.
|
Alpha Vantage 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 fuer Aktienkurse werden ueber <a href="/modules/setup/boersenchecker">dieses Modul-Setup</a> gepflegt.
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="margin-top:6px;">
|
||||||
|
FX-Provider, API-Key und Waehrungskatalog werden im Modul <a href="/module/fx-rates">fx-rates</a> gepflegt.
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin-top:12px;">
|
<div class="muted" style="margin-top:12px;">
|
||||||
Standard-Berichtswahrung: <?= e($defaultReportCurrency) ?> · Max. Alter: <?= e((string) $fxMaxAgeHours) ?>h
|
Standard-Berichtswahrung: <?= e($defaultReportCurrency) ?> · Max. Alter: <?= e((string) $fxMaxAgeHours) ?>h
|
||||||
|
|||||||
@@ -426,6 +426,11 @@ final class DashboardPage
|
|||||||
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.'));
|
||||||
}
|
}
|
||||||
|
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, [$quoteCurrency], $this->fxMaxAgeHours);
|
||||||
|
if (empty($fxResult['ok'])) {
|
||||||
|
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
|
||||||
|
}
|
||||||
|
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
|
||||||
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
|
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
|
||||||
$displayTime = (string) \module_fn(
|
$displayTime = (string) \module_fn(
|
||||||
'boersenchecker',
|
'boersenchecker',
|
||||||
@@ -443,7 +448,7 @@ final class DashboardPage
|
|||||||
(float) $apiResult['price'],
|
(float) $apiResult['price'],
|
||||||
$quoteCurrency,
|
$quoteCurrency,
|
||||||
(string) $apiResult['fetched_at'],
|
(string) $apiResult['fetched_at'],
|
||||||
(string) $apiResult['source']
|
(string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
|
||||||
);
|
);
|
||||||
if (!empty($storeResult['inserted'])) {
|
if (!empty($storeResult['inserted'])) {
|
||||||
return 'Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.';
|
return 'Alpha-Vantage-Kurs fuer ' . (string) $row['instrument_name'] . ' gespeichert.';
|
||||||
@@ -498,6 +503,16 @@ final class DashboardPage
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($bulkRows !== []) {
|
if ($bulkRows !== []) {
|
||||||
|
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
|
||||||
|
fn (array $row): string => $this->normalizeCurrency((string) ($row['quote_currency'] ?? '')),
|
||||||
|
$bulkRows
|
||||||
|
))));
|
||||||
|
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, $quoteCurrencies, $this->fxMaxAgeHours);
|
||||||
|
if (empty($fxResult['ok'])) {
|
||||||
|
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
|
||||||
|
}
|
||||||
|
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
|
||||||
|
|
||||||
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkRows);
|
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkRows);
|
||||||
if (empty($bulkResult['ok'])) {
|
if (empty($bulkResult['ok'])) {
|
||||||
throw new RuntimeException((string) ($bulkResult['message'] ?? 'Alpha-Vantage-Abruf fehlgeschlagen.'));
|
throw new RuntimeException((string) ($bulkResult['message'] ?? 'Alpha-Vantage-Abruf fehlgeschlagen.'));
|
||||||
@@ -526,7 +541,7 @@ final class DashboardPage
|
|||||||
(float) $apiResult['price'],
|
(float) $apiResult['price'],
|
||||||
$quoteCurrency,
|
$quoteCurrency,
|
||||||
(string) $apiResult['fetched_at'],
|
(string) $apiResult['fetched_at'],
|
||||||
(string) $apiResult['source']
|
(string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
|
||||||
);
|
);
|
||||||
if (!empty($storeResult['inserted'])) {
|
if (!empty($storeResult['inserted'])) {
|
||||||
$fetched++;
|
$fetched++;
|
||||||
@@ -698,7 +713,7 @@ final class DashboardPage
|
|||||||
|
|
||||||
if (is_array($latestQuote) && is_numeric($latestQuote['price'] ?? null)) {
|
if (is_array($latestQuote) && is_numeric($latestQuote['price'] ?? null)) {
|
||||||
$currentOriginal = $quantity * (float) $latestQuote['price'];
|
$currentOriginal = $quantity * (float) $latestQuote['price'];
|
||||||
$currentTotalBase = $this->convertAmount($currentOriginal, (string) $latestQuote['currency'], $baseCurrency);
|
$currentTotalBase = $this->convertAmount($currentOriginal, (string) $latestQuote['currency'], $baseCurrency, (string) ($latestQuote['source'] ?? ''));
|
||||||
$position['latest_price'] = (float) $latestQuote['price'];
|
$position['latest_price'] = (float) $latestQuote['price'];
|
||||||
$position['latest_currency'] = (string) $latestQuote['currency'];
|
$position['latest_currency'] = (string) $latestQuote['currency'];
|
||||||
$position['latest_quoted_at'] = (string) $latestQuote['quoted_at'];
|
$position['latest_quoted_at'] = (string) $latestQuote['quoted_at'];
|
||||||
@@ -796,7 +811,7 @@ final class DashboardPage
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertAmount(?float $amount, string $from, string $to): ?float
|
private function convertAmount(?float $amount, string $from, string $to, ?string $quoteSource = null): ?float
|
||||||
{
|
{
|
||||||
if ($amount === null) {
|
if ($amount === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -808,13 +823,9 @@ final class DashboardPage
|
|||||||
return $amount;
|
return $amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fxService = \module_fn('boersenchecker', 'fx_service');
|
|
||||||
if (!$fxService || !method_exists($fxService, 'convert')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$value = $fxService->convert($amount, $from, $to);
|
$fetchId = (int) (\module_fn('boersenchecker', 'fx_extract_fetch_id', (string) $quoteSource) ?? 0);
|
||||||
|
$value = \module_fn('boersenchecker', 'fx_convert_with_fetch', $amount, $from, $to, $fetchId > 0 ? $fetchId : null);
|
||||||
return is_numeric($value) ? (float) $value : null;
|
return is_numeric($value) ? (float) $value : null;
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ final class HomePage
|
|||||||
private string $positionTable;
|
private string $positionTable;
|
||||||
private string $quoteTable;
|
private string $quoteTable;
|
||||||
private string $defaultReportCurrency;
|
private string $defaultReportCurrency;
|
||||||
|
private float $fxMaxAgeHours;
|
||||||
private int $marketDataMinIntervalMinutes;
|
private int $marketDataMinIntervalMinutes;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
@@ -26,6 +27,10 @@ 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->fxMaxAgeHours = (float) ($settings['fx_max_age_hours'] ?? 6);
|
||||||
|
if ($this->fxMaxAgeHours <= 0) {
|
||||||
|
$this->fxMaxAgeHours = 6.0;
|
||||||
|
}
|
||||||
$this->marketDataMinIntervalMinutes = (int) (($settings['alpha_vantage_min_interval_minutes'] ?? null) ?: 60);
|
$this->marketDataMinIntervalMinutes = (int) (($settings['alpha_vantage_min_interval_minutes'] ?? null) ?: 60);
|
||||||
if ($this->marketDataMinIntervalMinutes <= 0) {
|
if ($this->marketDataMinIntervalMinutes <= 0) {
|
||||||
$this->marketDataMinIntervalMinutes = 60;
|
$this->marketDataMinIntervalMinutes = 60;
|
||||||
@@ -77,7 +82,8 @@ final class HomePage
|
|||||||
$currentReport = $this->convertAmount(
|
$currentReport = $this->convertAmount(
|
||||||
$currentNative,
|
$currentNative,
|
||||||
(string) ($position['latest_currency'] ?: ($position['quote_currency'] ?? $this->defaultReportCurrency)),
|
(string) ($position['latest_currency'] ?: ($position['quote_currency'] ?? $this->defaultReportCurrency)),
|
||||||
$this->defaultReportCurrency
|
$this->defaultReportCurrency,
|
||||||
|
(string) ($position['latest_source'] ?? '')
|
||||||
);
|
);
|
||||||
$position['current_total_report'] = $currentReport;
|
$position['current_total_report'] = $currentReport;
|
||||||
if ($position['purchase_total_report'] !== null && $currentReport !== null) {
|
if ($position['purchase_total_report'] !== null && $currentReport !== null) {
|
||||||
@@ -156,6 +162,16 @@ final class HomePage
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($bulkCandidates !== []) {
|
if ($bulkCandidates !== []) {
|
||||||
|
$quoteCurrencies = array_values(array_unique(array_filter(array_map(
|
||||||
|
fn (array $row): string => $this->normalizeCurrency((string) ($row['quote_currency'] ?? '')),
|
||||||
|
$bulkCandidates
|
||||||
|
))));
|
||||||
|
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, $quoteCurrencies, $this->fxMaxAgeHours);
|
||||||
|
if (empty($fxResult['ok'])) {
|
||||||
|
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
|
||||||
|
}
|
||||||
|
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
|
||||||
|
|
||||||
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkCandidates);
|
$bulkResult = \module_fn('boersenchecker', 'alpha_vantage_fetch_quotes', $bulkCandidates);
|
||||||
$quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : [];
|
$quotes = is_array($bulkResult['quotes'] ?? null) ? $bulkResult['quotes'] : [];
|
||||||
foreach ($bulkCandidates as $row) {
|
foreach ($bulkCandidates as $row) {
|
||||||
@@ -176,7 +192,7 @@ final class HomePage
|
|||||||
(float) $quote['price'],
|
(float) $quote['price'],
|
||||||
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
|
strtoupper(trim((string) ($quote['currency'] ?? $row['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
|
||||||
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
|
(string) ($quote['fetched_at'] ?? gmdate('Y-m-d H:i:s')),
|
||||||
(string) ($quote['source'] ?? 'alphavantage:global_quote')
|
(string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) ($quote['source'] ?? 'alphavantage:global_quote'), $fxFetchId)
|
||||||
);
|
);
|
||||||
if (!empty($storeResult['inserted'])) {
|
if (!empty($storeResult['inserted'])) {
|
||||||
$updated++;
|
$updated++;
|
||||||
@@ -319,7 +335,7 @@ final class HomePage
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function convertAmount(?float $amount, string $from, string $to): ?float
|
private function convertAmount(?float $amount, string $from, string $to, ?string $quoteSource = null): ?float
|
||||||
{
|
{
|
||||||
if ($amount === null) {
|
if ($amount === null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -331,13 +347,9 @@ final class HomePage
|
|||||||
return $amount;
|
return $amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fxService = \module_fn('boersenchecker', 'fx_service');
|
|
||||||
if (!$fxService || !method_exists($fxService, 'convert')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$value = $fxService->convert($amount, $from, $to);
|
$fetchId = (int) (\module_fn('boersenchecker', 'fx_extract_fetch_id', (string) $quoteSource) ?? 0);
|
||||||
|
$value = \module_fn('boersenchecker', 'fx_convert_with_fetch', $amount, $from, $to, $fetchId > 0 ? $fetchId : null);
|
||||||
return is_numeric($value) ? (float) $value : null;
|
return is_numeric($value) ? (float) $value : null;
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class InstrumentPage
|
|||||||
private string $positionTable;
|
private string $positionTable;
|
||||||
private string $quoteTable;
|
private string $quoteTable;
|
||||||
private string $defaultReportCurrency;
|
private string $defaultReportCurrency;
|
||||||
|
private float $fxMaxAgeHours;
|
||||||
private string $searchKeywords = '';
|
private string $searchKeywords = '';
|
||||||
private array $searchResults = [];
|
private array $searchResults = [];
|
||||||
private int $selectedInstrumentOverrideId = 0;
|
private int $selectedInstrumentOverrideId = 0;
|
||||||
@@ -28,6 +29,10 @@ final class InstrumentPage
|
|||||||
|
|
||||||
$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->fxMaxAgeHours = (float) ($settings['fx_max_age_hours'] ?? 6);
|
||||||
|
if ($this->fxMaxAgeHours <= 0) {
|
||||||
|
$this->fxMaxAgeHours = 6.0;
|
||||||
|
}
|
||||||
$table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name);
|
$table = static fn (string $name): string => \module_fn('boersenchecker', 'table', $name);
|
||||||
$this->instrumentTable = $table('instruments');
|
$this->instrumentTable = $table('instruments');
|
||||||
$this->positionTable = $table('positions');
|
$this->positionTable = $table('positions');
|
||||||
@@ -223,6 +228,12 @@ final class InstrumentPage
|
|||||||
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.'));
|
||||||
}
|
}
|
||||||
|
$quoteCurrency = strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency;
|
||||||
|
$fxResult = \module_fn('boersenchecker', 'fx_prepare_fetch', $this->defaultReportCurrency, [$quoteCurrency], $this->fxMaxAgeHours);
|
||||||
|
if (empty($fxResult['ok'])) {
|
||||||
|
throw new RuntimeException((string) ($fxResult['message'] ?? 'FX-Aktualisierung fehlgeschlagen.'));
|
||||||
|
}
|
||||||
|
$fxFetchId = is_numeric($fxResult['fetch_id'] ?? null) ? (int) $fxResult['fetch_id'] : null;
|
||||||
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
$latestApiQuote = $this->latestApiQuoteForInstrument($instrumentId);
|
||||||
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
|
if ($this->isApiSnapshotStale($latestApiQuote, (string) ($apiResult['fetched_at'] ?? ''))) {
|
||||||
$displayTime = (string) \module_fn(
|
$displayTime = (string) \module_fn(
|
||||||
@@ -239,9 +250,9 @@ final class InstrumentPage
|
|||||||
'store_market_quote',
|
'store_market_quote',
|
||||||
$instrumentId,
|
$instrumentId,
|
||||||
(float) $apiResult['price'],
|
(float) $apiResult['price'],
|
||||||
strtoupper(trim((string) ($instrument['quote_currency'] ?? $this->defaultReportCurrency))) ?: $this->defaultReportCurrency,
|
$quoteCurrency,
|
||||||
(string) $apiResult['fetched_at'],
|
(string) $apiResult['fetched_at'],
|
||||||
(string) $apiResult['source']
|
(string) \module_fn('boersenchecker', 'fx_source_with_fetch_id', (string) $apiResult['source'], $fxFetchId)
|
||||||
);
|
);
|
||||||
|
|
||||||
return !empty($storeResult['inserted'])
|
return !empty($storeResult['inserted'])
|
||||||
|
|||||||
Reference in New Issue
Block a user