boersenchecker
This commit is contained in:
402
modules/boersenchecker/bootstrap.php
Normal file
402
modules/boersenchecker/bootstrap.php
Normal file
@@ -0,0 +1,402 @@
|
||||
<?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,
|
||||
];
|
||||
});
|
||||
Reference in New Issue
Block a user