fix(mining): make "Est. Daily" honest about variance and fees
The mining "Est. Daily" was a solo expected-value (your hashrate share x
blocks/day x a fixed block reward) rendered in the same green as the real
Today / Yesterday / All-Time earnings, off an instantaneous hashrate and
gross of pool fees. It read like a dependable daily income when it is a
long-run statistical average with huge (Poisson) variance.
Framing:
- The estimate is now drawn in a neutral colour, not the green "money
earned" colour (the actual mined totals stay green).
- A sub-label in BOTH modes marks it as an average ("avg", or "avg, net"
in pool mode when the fee is applied), replacing the pool-only
"before pool fee" note.
- A hover tooltip over the column explains the variance ("often nothing
for long stretches, then a lump - NOT a steady daily income") and the
fee status (after the N% fee / before the fee when unknown / nothing for
solo and 0%-fee pools).
Accuracy:
- Uses a trailing-average hashrate instead of the instantaneous / 10s
snapshot: pool prefers hashrate_15m (then 60s, then 10s); solo uses the
mean of hashrate_history once there are >=10 samples, else the instant
rate. This stops a spike or a momentary dip from swinging the figure.
- Subtracts the known pool fee so pool-mode figures are net; synthetic or
unmatched pools stay gross and the tooltip says the fee is unknown.
The static block reward and true pool-reported earnings are intentionally
out of scope (the latter needs a new pool-API field). New i18n keys are
translated into all eight locales; the CJK subset font is rebuilt for the
three new glyphs. Builds + test suites pass on both variants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../util/pool_registry.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
@@ -68,15 +69,38 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
minedAllTimeCount = earn.allTimeCount;
|
||||
const std::vector<MiningEarningsRecentTx>& recentMined = earn.recent;
|
||||
|
||||
// Est. Daily = expected reward for YOUR own hashrate share of the network. In pool mode this uses
|
||||
// your local miner rate (pool_mining.hashrate_10s), NOT the pool's aggregate rate: pool payouts are
|
||||
// share-proportional, so your expected daily is the same solo-equivalent value (a rough estimate,
|
||||
// before the pool fee). Using the pool's total rate here would show the POOL's earnings, not yours. (M-05)
|
||||
double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate;
|
||||
// Est. Daily is a statistical long-run AVERAGE of your reward for your hashrate share of the
|
||||
// network -- NOT a steady income. Actual rewards arrive at random (Poisson): often nothing for
|
||||
// long stretches, then a lump. We feed EstimateHoursToBlock a TRAILING-AVERAGE hashrate (not an
|
||||
// instantaneous / 10s snapshot) so a spike or a momentary dip doesn't swing the figure, and in
|
||||
// pool mode we subtract the known pool fee so the number is net. Pool payouts are share-
|
||||
// proportional, so your expected daily equals this solo-equivalent value (using the pool's
|
||||
// aggregate rate would show the POOL's earnings, not yours). (M-05 / A+B)
|
||||
double estHashrate;
|
||||
if (s_pool_mode) {
|
||||
const auto& pm = state.pool_mining;
|
||||
estHashrate = pm.hashrate_15m > 0.0 ? pm.hashrate_15m
|
||||
: pm.hashrate_60s > 0.0 ? pm.hashrate_60s
|
||||
: pm.hashrate_10s; // 15m avg preferred; fall back as it warms up
|
||||
} else {
|
||||
estHashrate = mining.localHashrate;
|
||||
if (mining.hashrate_history.size() >= 10) { // enough samples -> trailing mean
|
||||
double sum = 0.0;
|
||||
for (double h : mining.hashrate_history) sum += h;
|
||||
estHashrate = sum / (double)mining.hashrate_history.size();
|
||||
}
|
||||
}
|
||||
double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
|
||||
double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0;
|
||||
double blockReward = schema::UI().drawElement("business", "block-reward").size;
|
||||
double estDaily = estDailyBlocks * blockReward;
|
||||
// Pool fee: compile-time known fee for the configured pool (<0 = unknown/synthetic pool -> gross).
|
||||
double estFeePct = -1.0;
|
||||
if (s_pool_mode) {
|
||||
const util::KnownPool* kp = util::findKnownPoolByUrl(state.pool_mining.pool_url);
|
||||
if (kp && kp->feePercent >= 0.0) estFeePct = kp->feePercent;
|
||||
}
|
||||
if (s_pool_mode && estFeePct > 0.0) estDaily *= (1.0 - estFeePct / 100.0);
|
||||
bool estActive = isMiningActive && estDaily > 0;
|
||||
|
||||
ImU32 greenCol2 = Success();
|
||||
@@ -185,15 +209,19 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
|
||||
else
|
||||
snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na"));
|
||||
// Disclose in pool mode that Est. Daily is a rough solo-equivalent (before the pool fee), so the
|
||||
// number isn't silently mismatched to its plain "Est. Daily" label. (M-05)
|
||||
const char* estSub = (s_pool_mode && estActive) ? TR("mining_est_daily_pool_sub") : nullptr;
|
||||
// Flag that Est. Daily is a long-run AVERAGE in both modes (so it doesn't read as realized
|
||||
// income), and whether it is net of the pool fee. Tooltip below carries the full variance note.
|
||||
const char* estSub = !estActive ? nullptr
|
||||
: (s_pool_mode && estFeePct > 0.0) ? TR("mining_est_daily_sub_net")
|
||||
: TR("mining_est_daily_sub");
|
||||
// Neutral (not green/"income") colour for the estimate — it's a probability, not money earned.
|
||||
const ImU32 estCol = estActive ? OnSurfaceMedium() : OnSurfaceDisabled();
|
||||
|
||||
EarningsEntry entries[] = {
|
||||
{ TR("mining_today"), todayVal, todaySub, greenCol2 },
|
||||
{ TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() },
|
||||
{ TR("mining_all_time"), allVal, allSub, OnSurface() },
|
||||
{ TR("mining_est_daily"), estVal, estSub, estActive ? greenCol2 : OnSurfaceDisabled() },
|
||||
{ TR("mining_est_daily"), estVal, estSub, estCol },
|
||||
};
|
||||
|
||||
for (int ei = 0; ei < numCols; ei++) {
|
||||
@@ -223,6 +251,27 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
ImVec2(centerX - valSz.x * 0.5f, valY), entries[ei].col, entries[ei].value);
|
||||
}
|
||||
}
|
||||
|
||||
// Hover tooltip over the Est. Daily column (last column): explain the Poisson variance and,
|
||||
// in pool mode, whether the figure is net of the fee.
|
||||
{
|
||||
float estColX = cardMin.x + pad + (numCols - 1) * colW;
|
||||
ImVec2 estMin(estColX, ey);
|
||||
ImVec2 estMax(estColX + colW, cardMin.y + earningsRowH);
|
||||
if (material::IsRectHovered(estMin, estMax)) {
|
||||
char tip[512];
|
||||
if (s_pool_mode && estFeePct > 0.0) {
|
||||
char feeNote[160];
|
||||
snprintf(feeNote, sizeof(feeNote), TR("tt_mining_est_daily_fee_fmt"), estFeePct);
|
||||
snprintf(tip, sizeof(tip), "%s\n%s", TR("tt_mining_est_daily"), feeNote);
|
||||
} else if (s_pool_mode && estFeePct < 0.0) { // fee unknown (synthetic/unmatched pool)
|
||||
snprintf(tip, sizeof(tip), "%s\n%s", TR("tt_mining_est_daily"), TR("tt_mining_est_daily_nofee"));
|
||||
} else { // solo, or a known 0%-fee pool (net == gross)
|
||||
snprintf(tip, sizeof(tip), "%s", TR("tt_mining_est_daily"));
|
||||
}
|
||||
material::Tooltip("%s", tip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Separator between earnings & details ===
|
||||
|
||||
@@ -2184,6 +2184,11 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_difficulty_copied"] = "Difficulty copied";
|
||||
strings_["mining_est_block"] = "Est. Block";
|
||||
strings_["mining_est_daily"] = "Est. Daily";
|
||||
strings_["mining_est_daily_sub"] = "avg";
|
||||
strings_["mining_est_daily_sub_net"] = "avg, net";
|
||||
strings_["tt_mining_est_daily"] = "A long-run statistical average for your current hashrate share of the network. Real mining rewards arrive at random (Poisson) - often nothing for long stretches, then a lump - so this is NOT a steady daily income.";
|
||||
strings_["tt_mining_est_daily_fee_fmt"] = "Shown after the %.1f%% pool fee.";
|
||||
strings_["tt_mining_est_daily_nofee"] = "Shown before the pool fee (fee unknown for this pool).";
|
||||
strings_["mining_filter_all"] = "All";
|
||||
strings_["mining_filter_tip_all"] = "Show all earnings";
|
||||
strings_["mining_filter_tip_pool"] = "Show pool earnings only";
|
||||
|
||||
Reference in New Issue
Block a user