feat: full CoinMarketCap-style crypto portfolio (buy/sell log, 1h/24h/7d%, P&L)

Replaces the plain quantity field with a CryptoTransaction log (same
"record events, derive the total" shape as Goal/GoalContribution): quantity
and average buy price are now computed via weighted-average cost — a buy
blends into the running average, a sell reduces quantity but leaves the
average cost of what's still held unchanged.

Switches price fetching from /simple/price to CoinGecko's /coins/markets
(still one batched call for every coin), which also returns 1h/24h/7d %
change in the same request. Old PATCH /crypto/holdings/{id} is replaced by
POST .../transactions (buy/sell, rejects selling more than held), GET
.../transactions (history), and DELETE /crypto/transactions/{id}.

Frontend: CryptoHoldingsTable replaces the simple list with a real table
(Name/Price/1h%/24h%/7d%/Holdings/Avg Buy Price/Profit-Loss/Actions),
horizontally scrollable on mobile rather than dropping columns. New
CryptoTransactionModal (buy/sell) and CryptoTransactionHistoryModal.

Backend: 79/79 tests passing (13 in test_crypto.py, up from 9). Also
verified live end-to-end against the real CoinGecko API with the user's own
key — create, buy, sell, oversell rejection, history, manual refresh, all
against real BTC/ETH prices in EUR — test data cleaned up after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
ZProger
2026-08-30 04:25:03 +03:00
parent 7e4508d1de
commit e8cd536584
24 changed files with 1154 additions and 374 deletions

View File

@@ -123,9 +123,29 @@ const ru = {
"crypto.search.empty": "Ничего не найдено.",
"crypto.search.noApiKey": "Не настроен ключ CoinGecko API — задайте AURUM_COINGECKO_API_KEY в .env (бесплатный ключ на coingecko.com/en/api/pricing).",
"crypto.form.quantityLabel": "Количество",
"crypto.form.pricePerUnitLabel": "Цена за штуку",
"crypto.form.dateLabel": "Дата",
"crypto.form.noteLabel": "Заметка (необязательно)",
"crypto.form.addTitle": "Добавить {{name}}",
"crypto.form.editTitle": "Изменить количество: {{name}}",
"crypto.form.tradeTitle": "Сделка: {{name}}",
"crypto.form.tradeButton": "Купить / продать",
"crypto.form.buy": "Покупка",
"crypto.form.sell": "Продажа",
"crypto.form.errorSellTooMuch": "Нельзя продать больше, чем у вас есть.",
"crypto.form.saveError": "Не удалось сохранить. Проверьте данные и попробуйте снова.",
"crypto.table.name": "Название",
"crypto.table.price": "Цена",
"crypto.table.change1h": "1ч%",
"crypto.table.change24h": "24ч%",
"crypto.table.change7d": "7д%",
"crypto.table.holdings": "Держите",
"crypto.table.avgBuyPrice": "Средняя цена покупки",
"crypto.table.profitLoss": "Прибыль/убыток",
"crypto.table.actions": "Действия",
"crypto.history.title": "История сделок: {{name}}",
"crypto.history.empty": "Пока нет ни одной сделки.",
"crypto.history.confirmDelete": "Удалить эту сделку? Количество и средняя цена будут пересчитаны.",
"crypto.history.viewButton": "Смотреть историю",
"roi.calculator.title": "Калькулятор ROI",
"roi.calculator.investmentLabel": "Сумма инвестиции",
@@ -551,9 +571,29 @@ const en: Record<keyof typeof ru, string> = {
"crypto.search.empty": "Nothing found.",
"crypto.search.noApiKey": "CoinGecko API key not configured — set AURUM_COINGECKO_API_KEY in .env (free key at coingecko.com/en/api/pricing).",
"crypto.form.quantityLabel": "Quantity",
"crypto.form.pricePerUnitLabel": "Price per unit",
"crypto.form.dateLabel": "Date",
"crypto.form.noteLabel": "Note (optional)",
"crypto.form.addTitle": "Add {{name}}",
"crypto.form.editTitle": "Edit quantity: {{name}}",
"crypto.form.tradeTitle": "Trade: {{name}}",
"crypto.form.tradeButton": "Buy / sell",
"crypto.form.buy": "Buy",
"crypto.form.sell": "Sell",
"crypto.form.errorSellTooMuch": "You can't sell more than you currently hold.",
"crypto.form.saveError": "Failed to save. Check the data and try again.",
"crypto.table.name": "Name",
"crypto.table.price": "Price",
"crypto.table.change1h": "1h%",
"crypto.table.change24h": "24h%",
"crypto.table.change7d": "7d%",
"crypto.table.holdings": "Holdings",
"crypto.table.avgBuyPrice": "Avg. Buy Price",
"crypto.table.profitLoss": "Profit/Loss",
"crypto.table.actions": "Actions",
"crypto.history.title": "Trade history: {{name}}",
"crypto.history.empty": "No trades yet.",
"crypto.history.confirmDelete": "Delete this trade? Quantity and average price will be recalculated.",
"crypto.history.viewButton": "View history",
"roi.calculator.title": "ROI Calculator",
"roi.calculator.investmentLabel": "Investment amount",