Miner-Upgrade
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user