684 lines
26 KiB
PHP
684 lines
26 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
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 {
|
|
$prefix = 'Modules\\Boersenchecker\\';
|
|
if (!str_starts_with($class, $prefix)) {
|
|
return;
|
|
}
|
|
|
|
$file = __DIR__ . '/src/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';
|
|
if (is_file($file)) {
|
|
require_once $file;
|
|
}
|
|
});
|
|
|
|
$moduleName = 'boersenchecker';
|
|
$mm = isset($modules) && $modules instanceof App\ModuleManager ? $modules : modules();
|
|
|
|
$mm->registerFunction($moduleName, 'table', static function (string $name): string {
|
|
$prefix = 'boersencheck_';
|
|
$sanitized = preg_replace('/[^a-zA-Z0-9_]/', '', $name);
|
|
return $prefix . $sanitized;
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'pdo', function () use ($moduleName): \PDO {
|
|
$settings = modules()->settings($moduleName);
|
|
$useSeparate = !empty($settings['use_separate_db']);
|
|
|
|
if ($useSeparate) {
|
|
$module = modules()->get($moduleName);
|
|
$fallback = $module['db_defaults'] ?? [];
|
|
return modules()->modulePdo($moduleName, $fallback);
|
|
}
|
|
|
|
$base = app()->basePdo();
|
|
if ($base instanceof \PDO) {
|
|
return $base;
|
|
}
|
|
|
|
throw new ModuleConfigException(
|
|
$moduleName,
|
|
'Base-DB ist deaktiviert. Bitte Base-DB aktivieren oder eine eigene Modul-DB konfigurieren.'
|
|
);
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'ensure_schema', function () use ($moduleName): void {
|
|
$pdo = module_fn($moduleName, 'pdo');
|
|
$table = static fn (string $name): string => module_fn($moduleName, 'table', $name);
|
|
$driver = strtolower((string) $pdo->getAttribute(PDO::ATTR_DRIVER_NAME));
|
|
|
|
$portfolioTable = $table('portfolios');
|
|
$instrumentTable = $table('instruments');
|
|
$positionTable = $table('positions');
|
|
$quoteTable = $table('quotes');
|
|
|
|
if ($driver === 'pgsql') {
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$portfolioTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
name VARCHAR(190) NOT NULL,
|
|
base_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
notes TEXT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$instrumentTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
isin VARCHAR(32) NULL,
|
|
wkn VARCHAR(32) NULL,
|
|
symbol VARCHAR(32) NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
quote_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
market VARCHAR(120) NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$positionTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
portfolio_id INTEGER NOT NULL,
|
|
instrument_id INTEGER NOT NULL,
|
|
quantity NUMERIC(20,6) NOT NULL,
|
|
purchase_price NUMERIC(20,8) NOT NULL,
|
|
purchase_currency VARCHAR(10) NOT NULL,
|
|
purchase_date DATE NOT NULL,
|
|
fees NUMERIC(20,8) NULL,
|
|
notes TEXT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$quoteTable} (
|
|
id SERIAL PRIMARY KEY,
|
|
instrument_id INTEGER NOT NULL,
|
|
price NUMERIC(20,8) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL,
|
|
quoted_at TIMESTAMP NOT NULL,
|
|
source VARCHAR(64) NOT NULL DEFAULT 'manual',
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$portfolioTable}_owner_idx ON {$portfolioTable} (owner_sub)");
|
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS {$instrumentTable}_isin_uniq ON {$instrumentTable} (isin) WHERE isin IS NOT NULL");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$instrumentTable}_symbol_idx ON {$instrumentTable} (symbol)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_owner_idx ON {$positionTable} (owner_sub)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_portfolio_idx ON {$positionTable} (portfolio_id)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_instrument_idx ON {$positionTable} (instrument_id)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$quoteTable}_instrument_time_idx ON {$quoteTable} (instrument_id, quoted_at DESC)");
|
|
} elseif ($driver === 'mysql') {
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$portfolioTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
name VARCHAR(190) NOT NULL,
|
|
base_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
notes TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
KEY {$portfolioTable}_owner_idx (owner_sub)
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$instrumentTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
isin VARCHAR(32) NULL,
|
|
wkn VARCHAR(32) NULL,
|
|
symbol VARCHAR(32) NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
quote_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
market VARCHAR(120) NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE KEY {$instrumentTable}_isin_uniq (isin),
|
|
KEY {$instrumentTable}_symbol_idx (symbol)
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$positionTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
portfolio_id INTEGER NOT NULL,
|
|
instrument_id INTEGER NOT NULL,
|
|
quantity DECIMAL(20,6) NOT NULL,
|
|
purchase_price DECIMAL(20,8) NOT NULL,
|
|
purchase_currency VARCHAR(10) NOT NULL,
|
|
purchase_date DATE NOT NULL,
|
|
fees DECIMAL(20,8) NULL,
|
|
notes TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
KEY {$positionTable}_owner_idx (owner_sub),
|
|
KEY {$positionTable}_portfolio_idx (portfolio_id),
|
|
KEY {$positionTable}_instrument_idx (instrument_id)
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$quoteTable} (
|
|
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
|
instrument_id INTEGER NOT NULL,
|
|
price DECIMAL(20,8) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL,
|
|
quoted_at DATETIME NOT NULL,
|
|
source VARCHAR(64) NOT NULL DEFAULT 'manual',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
KEY {$quoteTable}_instrument_time_idx (instrument_id, quoted_at)
|
|
)");
|
|
} else {
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$portfolioTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
name VARCHAR(190) NOT NULL,
|
|
base_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
notes TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$instrumentTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
isin VARCHAR(32) NULL,
|
|
wkn VARCHAR(32) NULL,
|
|
symbol VARCHAR(32) NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
quote_currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
|
|
market VARCHAR(120) NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$positionTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
owner_sub VARCHAR(190) NOT NULL,
|
|
portfolio_id INTEGER NOT NULL,
|
|
instrument_id INTEGER NOT NULL,
|
|
quantity DECIMAL(20,6) NOT NULL,
|
|
purchase_price DECIMAL(20,8) NOT NULL,
|
|
purchase_currency VARCHAR(10) NOT NULL,
|
|
purchase_date DATE NOT NULL,
|
|
fees DECIMAL(20,8) NULL,
|
|
notes TEXT NULL,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE TABLE IF NOT EXISTS {$quoteTable} (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
instrument_id INTEGER NOT NULL,
|
|
price DECIMAL(20,8) NOT NULL,
|
|
currency VARCHAR(10) NOT NULL,
|
|
quoted_at DATETIME NOT NULL,
|
|
source VARCHAR(64) NOT NULL DEFAULT 'manual',
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$portfolioTable}_owner_idx ON {$portfolioTable} (owner_sub)");
|
|
$pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS {$instrumentTable}_isin_uniq ON {$instrumentTable} (isin)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$instrumentTable}_symbol_idx ON {$instrumentTable} (symbol)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_owner_idx ON {$positionTable} (owner_sub)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_portfolio_idx ON {$positionTable} (portfolio_id)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$positionTable}_instrument_idx ON {$positionTable} (instrument_id)");
|
|
$pdo->exec("CREATE INDEX IF NOT EXISTS {$quoteTable}_instrument_time_idx ON {$quoteTable} (instrument_id, quoted_at DESC)");
|
|
}
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'fx_service', static function (): ?object {
|
|
if (!is_dir(dirname(__DIR__) . '/mining-checker')) {
|
|
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;
|
|
}
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'fx_refresh', static function (string $baseCurrency = 'EUR', float $maxAgeHours = 6.0): array {
|
|
$service = module_fn('boersenchecker', 'fx_service');
|
|
if (!$service || !method_exists($service, 'ensureFreshLatestRates')) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Mining-Checker FX-Service ist aktuell nicht verfuegbar.',
|
|
];
|
|
}
|
|
|
|
try {
|
|
$result = $service->ensureFreshLatestRates($maxAgeHours, strtoupper(trim($baseCurrency)) ?: 'EUR');
|
|
return [
|
|
'ok' => true,
|
|
'message' => !empty($result['reused'])
|
|
? 'Vorhandene FX-Daten weiterverwendet.'
|
|
: 'FX-Daten aus dem Mining-Checker aktualisiert.',
|
|
'result' => $result,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'FX-Aktualisierung fehlgeschlagen: ' . $e->getMessage(),
|
|
];
|
|
}
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'alpha_vantage_fetch_quote', static function (string $symbol): array {
|
|
$settings = modules()->settings('boersenchecker');
|
|
$apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? ''));
|
|
$timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12);
|
|
$timeout = $timeout > 0 ? $timeout : 12;
|
|
$symbol = strtoupper(trim($symbol));
|
|
|
|
if ($symbol === '') {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Kein API-Symbol hinterlegt.',
|
|
];
|
|
}
|
|
|
|
if ($apiKey === '') {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Alpha-Vantage-API-Key fehlt. Bitte im Modul-Setup hinterlegen.',
|
|
];
|
|
}
|
|
|
|
$url = 'https://www.alphavantage.co/query?' . http_build_query([
|
|
'function' => 'GLOBAL_QUOTE',
|
|
'symbol' => $symbol,
|
|
'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 Anfrage fehlgeschlagen.'
|
|
. ($curlError !== '' ? ' ' . $curlError : '')
|
|
. ($httpCode > 0 ? ' HTTP ' . $httpCode : ''),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
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 Anfrage lieferte keine Daten.',
|
|
];
|
|
}
|
|
}
|
|
|
|
$decoded = json_decode($responseBody, true);
|
|
if (!is_array($decoded)) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Alpha Vantage Antwort ist kein gueltiges JSON.',
|
|
];
|
|
}
|
|
|
|
if (!empty($decoded['Note'])) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Alpha Vantage Limit-Hinweis: ' . trim((string) $decoded['Note']),
|
|
];
|
|
}
|
|
|
|
if (!empty($decoded['Information'])) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => trim((string) $decoded['Information']),
|
|
];
|
|
}
|
|
|
|
if (!empty($decoded['Error Message'])) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => trim((string) $decoded['Error Message']),
|
|
];
|
|
}
|
|
|
|
$quote = is_array($decoded['Global Quote'] ?? null) ? $decoded['Global Quote'] : [];
|
|
$price = $quote['05. price'] ?? null;
|
|
if (!is_numeric($price)) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => 'Alpha Vantage lieferte keinen Preis fuer das Symbol ' . $symbol . '.',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => true,
|
|
'symbol' => (string) ($quote['01. symbol'] ?? $symbol),
|
|
'price' => (float) $price,
|
|
'latest_trading_day' => (string) ($quote['07. latest trading day'] ?? ''),
|
|
'previous_close' => is_numeric($quote['08. previous close'] ?? null) ? (float) $quote['08. previous close'] : null,
|
|
'change' => is_numeric($quote['09. change'] ?? null) ? (float) $quote['09. change'] : null,
|
|
'change_percent' => (string) ($quote['10. change percent'] ?? ''),
|
|
'fetched_at' => date('Y-m-d H:i:s'),
|
|
'source' => 'alpha_vantage:GLOBAL_QUOTE',
|
|
'raw' => $quote,
|
|
];
|
|
});
|
|
|
|
$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,
|
|
];
|
|
});
|
|
|
|
$mm->registerFunction($moduleName, 'alpha_vantage_fetch_chart_series', static function (string $symbol): array {
|
|
$settings = modules()->settings('boersenchecker');
|
|
$apiKey = trim((string) ($settings['alpha_vantage_api_key'] ?? ''));
|
|
$timeout = (int) ($settings['alpha_vantage_timeout_sec'] ?? 12);
|
|
$timeout = $timeout > 0 ? $timeout : 12;
|
|
$symbol = strtoupper(trim($symbol));
|
|
|
|
if ($symbol === '') {
|
|
return ['ok' => false, 'message' => 'Kein Symbol angegeben.'];
|
|
}
|
|
if ($apiKey === '') {
|
|
return ['ok' => false, 'message' => 'Alpha-Vantage-API-Key fehlt.'];
|
|
}
|
|
|
|
$cacheDir = sys_get_temp_dir() . '/boersenchecker-alpha-vantage';
|
|
if (!is_dir($cacheDir)) {
|
|
@mkdir($cacheDir, 0775, true);
|
|
}
|
|
|
|
$fetchPayload = static function (string $functionName, int $ttl) use ($symbol, $apiKey, $timeout, $cacheDir): array {
|
|
$cacheKey = md5($functionName . '|' . $symbol . '|' . $apiKey);
|
|
$cachePath = $cacheDir . '/' . $cacheKey . '.json';
|
|
if (is_file($cachePath) && (time() - filemtime($cachePath)) < $ttl) {
|
|
$cached = file_get_contents($cachePath);
|
|
$decoded = is_string($cached) ? json_decode($cached, true) : null;
|
|
if (is_array($decoded)) {
|
|
return $decoded;
|
|
}
|
|
}
|
|
|
|
$url = 'https://www.alphavantage.co/query?' . http_build_query([
|
|
'function' => $functionName,
|
|
'symbol' => $symbol,
|
|
'apikey' => $apiKey,
|
|
'outputsize' => 'compact',
|
|
]);
|
|
|
|
$responseBody = null;
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($url);
|
|
if ($ch !== false) {
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => $timeout,
|
|
CURLOPT_CONNECTTIMEOUT => min(5, $timeout),
|
|
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
|
]);
|
|
$responseBody = curl_exec($ch);
|
|
curl_close($ch);
|
|
}
|
|
}
|
|
|
|
if (!is_string($responseBody) || $responseBody === '') {
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'GET',
|
|
'timeout' => $timeout,
|
|
'header' => "Accept: application/json\r\n",
|
|
],
|
|
]);
|
|
$responseBody = @file_get_contents($url, false, $context);
|
|
}
|
|
|
|
$decoded = is_string($responseBody) ? json_decode($responseBody, true) : null;
|
|
if (is_array($decoded) && $decoded !== []) {
|
|
@file_put_contents($cachePath, json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
|
return $decoded;
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
$normalizeSeries = static function (array $payload, array $keys): array {
|
|
foreach ($keys as $key) {
|
|
$series = $payload[$key] ?? null;
|
|
if (!is_array($series)) {
|
|
continue;
|
|
}
|
|
|
|
$points = [];
|
|
foreach ($series as $date => $row) {
|
|
if (!is_array($row)) {
|
|
continue;
|
|
}
|
|
$close = $row['4. close'] ?? $row['5. adjusted close'] ?? null;
|
|
if (!is_numeric($close)) {
|
|
$close = $row['5. adjusted close'] ?? $row['4. close'] ?? null;
|
|
}
|
|
if (!is_numeric($close)) {
|
|
continue;
|
|
}
|
|
$points[] = [
|
|
'date' => (string) $date,
|
|
'close' => (float) $close,
|
|
];
|
|
}
|
|
|
|
usort($points, static fn (array $left, array $right): int => strcmp((string) $left['date'], (string) $right['date']));
|
|
return $points;
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
$dailyPayload = $fetchPayload('TIME_SERIES_DAILY_ADJUSTED', 6 * 3600);
|
|
if (($dailyPayload['Information'] ?? null) || ($dailyPayload['Error Message'] ?? null)) {
|
|
$dailyPayload = $fetchPayload('TIME_SERIES_DAILY', 6 * 3600);
|
|
}
|
|
$weeklyPayload = $fetchPayload('TIME_SERIES_WEEKLY_ADJUSTED', 12 * 3600);
|
|
if (($weeklyPayload['Information'] ?? null) || ($weeklyPayload['Error Message'] ?? null)) {
|
|
$weeklyPayload = $fetchPayload('TIME_SERIES_WEEKLY', 12 * 3600);
|
|
}
|
|
$monthlyPayload = $fetchPayload('TIME_SERIES_MONTHLY_ADJUSTED', 24 * 3600);
|
|
if (($monthlyPayload['Information'] ?? null) || ($monthlyPayload['Error Message'] ?? null)) {
|
|
$monthlyPayload = $fetchPayload('TIME_SERIES_MONTHLY', 24 * 3600);
|
|
}
|
|
|
|
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 [
|
|
'ok' => true,
|
|
'symbol' => $symbol,
|
|
'daily' => $daily,
|
|
'weekly' => $weekly,
|
|
'monthly' => $monthly,
|
|
];
|
|
});
|