Miner-Upgrade
All checks were successful
Deploy / deploy-staging (push) Successful in 7s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-09 00:58:48 +02:00
parent ee5a46254f
commit fc95898a9d
11 changed files with 976 additions and 131 deletions

View File

@@ -34,23 +34,26 @@ final class AnalyticsService
$previousIntervalRate = null;
$result = [];
$payoutIndex = 0;
$cumulativePayouts = 0.0;
$lastPayoutTs = null;
$payoutsByAsset = [];
$latestPriceByCurrency = [];
foreach ($measurements as $row) {
$measuredTs = $this->utcTimestamp((string) $row['measured_at']);
$coinCurrency = strtoupper(trim((string) ($row['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')));
while (isset($payouts[$payoutIndex])) {
$payoutTs = $this->utcTimestamp((string) ($payouts[$payoutIndex]['payout_at'] ?? ''));
if ($payoutTs <= 0 || $payoutTs > $measuredTs) {
break;
}
$cumulativePayouts += (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
$payoutAsset = strtoupper(trim((string) ($payouts[$payoutIndex]['payout_currency'] ?? $coinCurrency)));
$payoutsByAsset[$payoutAsset] = ($payoutsByAsset[$payoutAsset] ?? 0.0) + (float) ($payouts[$payoutIndex]['coins_amount'] ?? 0);
$lastPayoutTs = $payoutTs;
$payoutIndex++;
}
$cumulativePayouts = (float) ($payoutsByAsset[$coinCurrency] ?? 0.0);
$visibleCoinsTotal = (float) $row['coins_total'];
$effectiveCoinsTotal = $visibleCoinsTotal + $cumulativePayouts;
$growth = $effectiveCoinsTotal - $baselineCoins;
@@ -120,7 +123,7 @@ final class AnalyticsService
}
if ($price === null) {
foreach (['USD', 'EUR'] as $fallbackCurrency) {
$fxPrice = $this->convertAmount(1.0, 'DOGE', $fallbackCurrency, $row);
$fxPrice = $this->convertAmount(1.0, $coinCurrency, $fallbackCurrency, $row);
if ($fxPrice !== null && $fxPrice > 0) {
$latestPriceByCurrency[$fallbackCurrency] = $fxPrice;
}
@@ -159,6 +162,7 @@ final class AnalyticsService
$result[] = array_merge($row, [
'coins_total_visible' => $this->roundOrNull($visibleCoinsTotal, 6),
'coins_total_effective' => $this->roundOrNull($effectiveCoinsTotal, 6),
'coin_currency' => $coinCurrency,
'payouts_cumulative' => $this->roundOrNull($cumulativePayouts, 6),
'growth_since_baseline' => $this->roundOrNull($growth, 6),
'hours_since_baseline' => $this->roundOrNull($hoursSinceBaseline, 4),
@@ -296,26 +300,60 @@ final class AnalyticsService
}
$latestCurrency = (string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? '');
$investedCapital = $latestCurrency !== ''
$latestMeasuredTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? ''));
$cashInvestedCapital = $latestCurrency !== ''
? $this->totalInvestmentBasis(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$this->utcTimestamp((string) ($latest['measured_at'] ?? '')),
$latestMeasuredTs,
$latestCurrency,
$latest
$latest,
'cash'
)
: null;
$reinvestedCapital = $latestCurrency !== ''
? $this->totalInvestmentBasis(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$latestMeasuredTs,
$latestCurrency,
$latest,
'reinvest'
)
: null;
$walletBalances = $this->walletBalances($payouts, $purchasedMiners, $latestMeasuredTs);
$walletBalanceCurrentAsset = (float) ($walletBalances[strtoupper(trim((string) ($latest['coin_currency'] ?? $settings['crypto_currency'] ?? 'DOGE')))] ?? 0.0);
$holdingsCurrentAsset = $walletBalanceCurrentAsset + (float) ($latest['coins_total_visible'] ?? $latest['coins_total'] ?? 0);
$walletValue = $latestCurrency !== '' ? $this->walletBalanceValue($walletBalances, $latestCurrency, $latest) : null;
$currentVisibleValue = is_numeric($latest['current_value'] ?? null) ? (float) $latest['current_value'] : null;
$totalHoldingsValue = ($walletValue !== null || $currentVisibleValue !== null)
? (float) ($walletValue ?? 0.0) + (float) ($currentVisibleValue ?? 0.0)
: null;
$currentDailyRevenue = is_numeric($latest['theoretical_daily_revenue'] ?? null) ? (float) $latest['theoretical_daily_revenue'] : null;
$breakEvenRemainingAmount = $investedCapital;
$breakEvenDaysOverall = (
$investedCapital !== null &&
$currentDailyRevenue !== null &&
$currentDailyRevenue > 0
) ? ($investedCapital / $currentDailyRevenue) : null;
$breakEvenRemainingAmount = ($cashInvestedCapital !== null && $totalHoldingsValue !== null)
? max(0.0, $cashInvestedCapital - $totalHoldingsValue)
: $cashInvestedCapital;
$breakEvenProjection = (
$cashInvestedCapital !== null &&
$breakEvenRemainingAmount !== null
) ? $this->projectBreakEvenDate(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
$purchasedMiners,
$latest,
$breakEvenRemainingAmount
) : ['days' => null, 'eta' => null];
$breakEvenDaysOverall = is_numeric($breakEvenProjection['days'] ?? null) ? (float) $breakEvenProjection['days'] : null;
$latestSummary = array_merge($latest, [
'invested_capital' => $this->roundOrNull($investedCapital, 8),
'invested_capital' => $this->roundOrNull($cashInvestedCapital, 8),
'cash_invested_capital' => $this->roundOrNull($cashInvestedCapital, 8),
'reinvested_capital' => $this->roundOrNull($reinvestedCapital, 8),
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
'wallet_value' => $this->roundOrNull($walletValue, 8),
'total_holdings_value' => $this->roundOrNull($totalHoldingsValue, 8),
'break_even_remaining_amount' => $this->roundOrNull($breakEvenRemainingAmount, 8),
'break_even_days_overall' => $this->roundOrNull($breakEvenDaysOverall, 4),
'break_even_eta_at' => $breakEvenProjection['eta'] ?? null,
]);
$currentProjection = $this->projectPerformance(
is_array($settings['cost_plans'] ?? null) ? $settings['cost_plans'] : [],
@@ -349,6 +387,9 @@ final class AnalyticsService
'total_coins' => $this->roundOrNull(array_sum(array_map(static fn (array $payout): float => (float) ($payout['coins_amount'] ?? 0), $payouts)), 6),
'current_visible_coins' => $this->roundOrNull((float) ($latest['coins_total_visible'] ?? $latest['coins_total']), 6),
'current_effective_coins' => $this->roundOrNull((float) ($latest['coins_total_effective'] ?? $latest['coins_total']), 6),
'wallet_balances' => $walletBalances,
'wallet_balance_current_asset' => $this->roundOrNull($walletBalanceCurrentAsset, 6),
'holdings_current_asset' => $this->roundOrNull($holdingsCurrentAsset, 6),
],
'current_hashrate_mh' => $this->roundOrNull($currentHashrateMh, 4),
'miner_offers' => $offerSummary,
@@ -628,7 +669,7 @@ final class AnalyticsService
if (array_key_exists('is_active', $miner) && empty($miner['is_active'])) {
continue;
}
if (!$this->entryIsCovered($miner, $measurementTs > 0 ? $measurementTs : null, 'purchased_at')) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($miner['purchased_at'] ?? '')) > $measurementTs) {
continue;
}
@@ -650,7 +691,7 @@ final class AnalyticsService
return $matched ? $total : null;
}
private function totalInvestmentBasis(array $costPlans, array $purchasedMiners, int $measurementTs, string $targetCurrency, ?array $fxContext = null): ?float
private function totalInvestmentBasis(array $costPlans, array $purchasedMiners, int $measurementTs, string $targetCurrency, ?array $fxContext = null, ?string $fundingSource = null): ?float
{
$target = strtoupper(trim($targetCurrency));
if ($target === '') {
@@ -661,26 +702,16 @@ final class AnalyticsService
$matched = false;
$purchasedTotal = $this->totalPurchasedMinerCost($purchasedMiners, $target, $measurementTs, $fxContext);
if ($purchasedTotal !== null) {
if ($fundingSource === null && $purchasedTotal !== null) {
$matched = true;
$total += $purchasedTotal;
}
foreach ($costPlans as $plan) {
if (empty($plan['is_active'])) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($plan['starts_at'] ?? '')) > $measurementTs) {
continue;
}
$startTs = $this->utcTimestamp((string) ($plan['starts_at'] ?? ''));
$runtimeMonths = (int) ($plan['runtime_months'] ?? 0);
if ($startTs <= 0 || $runtimeMonths <= 0 || ($measurementTs > 0 && $measurementTs < $startTs)) {
continue;
}
$runtimeDays = $runtimeMonths * 30.4375;
$endTs = (int) round($startTs + ($runtimeDays * 86400));
$isCovered = !empty($plan['auto_renew']) || $measurementTs <= 0 || $measurementTs <= $endTs;
if (!$isCovered) {
if ($fundingSource !== null && $this->entryFundingSource($plan) !== $fundingSource) {
continue;
}
@@ -699,6 +730,31 @@ final class AnalyticsService
$total += $converted;
}
if ($fundingSource !== null) {
foreach ($purchasedMiners as $miner) {
if ($measurementTs > 0 && $this->utcTimestamp((string) ($miner['purchased_at'] ?? '')) > $measurementTs) {
continue;
}
if ($this->entryFundingSource($miner) !== $fundingSource) {
continue;
}
$amount = $this->investmentBasisAmount($miner);
$currency = strtoupper(trim((string) ($miner['reference_price_currency'] ?? $miner['currency'] ?? '')));
if ($amount === null || $amount <= 0 || $currency === '') {
continue;
}
$converted = $this->convertAmount($amount, $currency, $target, $fxContext);
if ($converted === null) {
continue;
}
$matched = true;
$total += $converted;
}
}
return $matched ? $total : null;
}
@@ -1014,6 +1070,153 @@ final class AnalyticsService
return $endIndex - $startIndex + 1;
}
private function walletBalances(array $payouts, array $purchasedMiners, int $measurementTs): array
{
$balances = [];
foreach ($payouts as $payout) {
$payoutTs = $this->utcTimestamp((string) ($payout['payout_at'] ?? ''));
if ($measurementTs > 0 && $payoutTs > $measurementTs) {
continue;
}
$currency = strtoupper(trim((string) ($payout['payout_currency'] ?? '')));
$amount = is_numeric($payout['coins_amount'] ?? null) ? (float) $payout['coins_amount'] : null;
if ($currency === '' || $amount === null) {
continue;
}
$balances[$currency] = ($balances[$currency] ?? 0.0) + $amount;
}
foreach ($purchasedMiners as $miner) {
$purchaseTs = $this->utcTimestamp((string) ($miner['purchased_at'] ?? ''));
if ($measurementTs > 0 && $purchaseTs > $measurementTs) {
continue;
}
if ($this->entryFundingSource($miner) !== 'reinvest') {
continue;
}
$currency = strtoupper(trim((string) ($miner['currency'] ?? '')));
$amount = is_numeric($miner['total_cost_amount'] ?? null) ? (float) $miner['total_cost_amount'] : null;
if ($currency === '' || $amount === null) {
continue;
}
$balances[$currency] = ($balances[$currency] ?? 0.0) - $amount;
}
ksort($balances);
return array_map(fn (float $value): float => round($value, 8), $balances);
}
private function walletBalanceValue(array $balances, string $targetCurrency, ?array $fxContext = null): ?float
{
$target = strtoupper(trim($targetCurrency));
if ($target === '' || $balances === []) {
return null;
}
$total = 0.0;
$matched = false;
foreach ($balances as $currency => $amount) {
if (!is_numeric($amount)) {
continue;
}
$numericAmount = (float) $amount;
if (strtoupper((string) $currency) === $target) {
$matched = true;
$total += $numericAmount;
continue;
}
$converted = $this->convertAmount($numericAmount, (string) $currency, $target, $fxContext);
if ($converted === null) {
continue;
}
$matched = true;
$total += $converted;
}
return $matched ? $total : null;
}
private function entryFundingSource(array $entry): string
{
return !empty($entry['auto_renew']) ? 'cash' : 'reinvest';
}
private function investmentBasisAmount(array $entry): ?float
{
if (is_numeric($entry['reference_price_amount'] ?? null)) {
return (float) $entry['reference_price_amount'];
}
if (is_numeric($entry['base_price_amount'] ?? null)) {
return (float) $entry['base_price_amount'];
}
if (is_numeric($entry['total_cost_amount'] ?? null)) {
return (float) $entry['total_cost_amount'];
}
return null;
}
private function projectBreakEvenDate(array $costPlans, array $purchasedMiners, array $latest, float $remainingAmount): array
{
if ($remainingAmount <= 0) {
return ['days' => 0.0, 'eta' => (string) ($latest['measured_at'] ?? null)];
}
$currency = strtoupper(trim((string) ($latest['effective_price_currency'] ?? $latest['price_currency'] ?? '')));
$pricePerCoin = is_numeric($latest['effective_price_per_coin'] ?? null) ? (float) $latest['effective_price_per_coin'] : null;
$dogePerDay = is_numeric($latest['doge_per_day_interval'] ?? null) ? (float) $latest['doge_per_day_interval'] : null;
$currentHashrateMh = $this->totalHashrateMh(array_merge($costPlans, $purchasedMiners));
$baseTs = $this->utcTimestamp((string) ($latest['measured_at'] ?? ''));
if ($currency === '' || $pricePerCoin === null || $dogePerDay === null || $currentHashrateMh <= 0 || $baseTs <= 0) {
return ['days' => null, 'eta' => null];
}
$dogePerDayPerMh = $dogePerDay / $currentHashrateMh;
$cumulativeRevenue = 0.0;
$maxDays = 3650;
for ($day = 0; $day <= $maxDays; $day++) {
$dayHashrate = 0.0;
$dayTs = $baseTs + ($day * 86400);
foreach ($costPlans as $plan) {
if (!empty($plan['is_active']) && $this->entryIsCovered($plan, $dayTs)) {
$dayHashrate += $this->normalizeHashrateMh($plan['mining_speed_value'] ?? null, $plan['mining_speed_unit'] ?? null);
$dayHashrate += $this->normalizeHashrateMh($plan['bonus_speed_value'] ?? null, $plan['bonus_speed_unit'] ?? null);
}
}
foreach ($purchasedMiners as $miner) {
if ((array_key_exists('is_active', $miner) && empty($miner['is_active'])) || !$this->entryIsCovered($miner, $dayTs, 'purchased_at')) {
continue;
}
$dayHashrate += $this->normalizeHashrateMh($miner['mining_speed_value'] ?? null, $miner['mining_speed_unit'] ?? null);
$dayHashrate += $this->normalizeHashrateMh($miner['bonus_speed_value'] ?? null, $miner['bonus_speed_unit'] ?? null);
}
if ($dayHashrate <= 0) {
continue;
}
$dayRevenue = $dayHashrate * $dogePerDayPerMh * $pricePerCoin;
$cumulativeRevenue += $dayRevenue;
if ($cumulativeRevenue >= $remainingAmount) {
$etaTs = (int) round($baseTs + ($day * 86400));
return [
'days' => (float) $day,
'eta' => $this->formatUtcTimestamp($etaTs),
];
}
}
return ['days' => null, 'eta' => null];
}
private function utcTimestamp(?string $value): int
{
$normalized = trim((string) $value);