fix: график крипты открывать за всё время, лучший/худший рост — по выбранному периоду

График во вкладке «Криптовалюта» теперь по умолчанию показывает всю
историю, а не 30 дней — короткое окно у портфеля, растущего годами,
часто попадало на локальную просадку и выглядело демотивирующе.

Плитки «Лучший рост» и «Худшее падение» раньше всегда считались за
24 часа. Теперь они следуют за диапазоном, выбранным у графика
(7д/30д/90д/всё время): для 7д/30д используются закэшированные данные
CoinGecko, для «всё время» — метрика за 1 год (дальше бесплатный
тариф CoinGecko историю не отдаёт), а для 90д — отдельный запрос по
каждой монете, который выполняется только когда пользователь реально
выбрал этот период.
This commit is contained in:
ZProger
2026-09-04 02:18:31 +03:00
parent aefd6cc035
commit cd125e9e45
15 changed files with 404 additions and 33 deletions

View File

@@ -136,6 +136,10 @@ class CryptoHoldingBackup(BaseModel):
price_change_1h: Decimal | None = None
price_change_24h: Decimal | None = None
price_change_7d: Decimal | None = None
# Defaulted so a backup exported before 30d/1y existed still imports
# cleanly under the same format version.
price_change_30d: Decimal | None = None
price_change_1y: Decimal | None = None
class CryptoTransactionBackup(BaseModel):

View File

@@ -93,6 +93,11 @@ class CryptoHoldingRead(BaseModel):
price_change_1h: Decimal | None
price_change_24h: Decimal | None
price_change_7d: Decimal | None
price_change_30d: Decimal | None
# Stands in for "all time" on the Best/Worst Performer stat — CoinGecko's
# free tier caps historical lookback at 365 days regardless (see
# services/crypto_service.py's module docstring).
price_change_1y: Decimal | None
# All derived from the above: value = current_price * quantity,
# cost_basis = avg_buy_price * quantity, profit_loss = value - cost_basis.
@@ -102,6 +107,25 @@ class CryptoHoldingRead(BaseModel):
profit_loss_percent: float | None
class CryptoPerformancePoint(BaseModel):
asset_id: int
# None when CoinGecko's 90-day chart couldn't be fetched for this coin
# (rate-limited, unreachable, or the coin simply lacks 90 days of
# history) — the item is still listed so the frontend can render "—"
# for that one coin instead of the whole tile disappearing.
price_change_percent: float | None
class CryptoPerformanceResponse(BaseModel):
"""Real 90-day % price change per currently-held coin, for the Crypto
tab's Best/Worst Performer stat when the 90d range is selected — the one
window CoinGecko's batched /coins/markets call can't give us (see
services/crypto_service.py's get_90d_performance), so this is fetched
on demand, once, only when a viewer actually picks 90d."""
items: list[CryptoPerformancePoint]
class CryptoSyncResult(BaseModel):
# False when the lazy daily check ran but the 24h window hadn't
# elapsed yet (nothing called CoinGecko this time, error_key is None),