mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-12 18:46:40 +07:00
feat: сеть (network) для крипто-холдингов
Свободное текстовое поле на холдинге — какой сети принадлежит конкретная монета (Ethereum, Tron и т.д.), с редактированием прямо в таблице и разбивкой % по сетям на отдельной вкладке. Помогает увидеть, что часть стейблкоинов лежит на рискованных сетях и стоит перевести на безопасные.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
"""add network field to crypto holdings
|
||||
|
||||
Revision ID: d1a6f4c8b729
|
||||
Revises: c4e8f61a9d23
|
||||
Create Date: 2026-09-08 00:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'd1a6f4c8b729'
|
||||
down_revision: Union[str, None] = 'c4e8f61a9d23'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('crypto_holdings', sa.Column('network', sa.String(length=50), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('crypto_holdings', 'network')
|
||||
@@ -6,6 +6,7 @@ from app.schemas.crypto import (
|
||||
CryptoHistoryResponse,
|
||||
CryptoHoldingCreate,
|
||||
CryptoHoldingRead,
|
||||
CryptoHoldingUpdate,
|
||||
CryptoPerformanceResponse,
|
||||
CryptoPortfolioCreate,
|
||||
CryptoPortfolioRead,
|
||||
@@ -29,6 +30,7 @@ from app.services.crypto_service import (
|
||||
list_transactions,
|
||||
refresh_prices,
|
||||
search_coins,
|
||||
update_holding,
|
||||
update_portfolio,
|
||||
update_transaction,
|
||||
)
|
||||
@@ -89,6 +91,15 @@ async def create_holding_route(
|
||||
return await create_holding(session, payload)
|
||||
|
||||
|
||||
@router.patch("/holdings/{asset_id}", response_model=CryptoHoldingRead)
|
||||
async def update_holding_route(
|
||||
asset_id: int, payload: CryptoHoldingUpdate, session: AsyncSession = Depends(get_session)
|
||||
) -> CryptoHoldingRead:
|
||||
"""Updates holding-only metadata (currently just `network`) — quantity/
|
||||
price/date go through the /transactions routes below instead."""
|
||||
return await update_holding(session, asset_id, payload)
|
||||
|
||||
|
||||
@router.post("/holdings/{asset_id}/transactions", response_model=CryptoHoldingRead, status_code=201)
|
||||
async def add_transaction_route(
|
||||
asset_id: int, payload: CryptoTransactionCreate, session: AsyncSession = Depends(get_session)
|
||||
|
||||
@@ -75,6 +75,14 @@ class CryptoHolding(Base, TimestampMixin):
|
||||
symbol: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(150), nullable=False)
|
||||
thumb_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
# Free-form, user-entered — which chain this specific holding actually
|
||||
# sits on (e.g. "Ethereum", "Tron", "Arbitrum"). Deliberately not an
|
||||
# enum: CoinGecko doesn't expose per-holding chain data (the same
|
||||
# coingecko_id like "tether" covers the token across every chain it's
|
||||
# issued on), and new chains appear too often for a fixed list to keep
|
||||
# up. Lets a stablecoin held across several networks be told apart by
|
||||
# counterparty/bridge risk, which risk_level alone can't capture.
|
||||
network: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
|
||||
# Cached from the last successful sync (services/crypto_service.py) —
|
||||
# in the app's display currency (see AppSettings.currency). Null until
|
||||
|
||||
@@ -76,6 +76,18 @@ class CryptoHoldingCreate(BaseModel):
|
||||
# services/crypto_service.py) — but user-overridable at add time, since
|
||||
# not every coin carries the same risk (a large-cap vs. a memecoin).
|
||||
risk_level: RiskLevel = RiskLevel.HIGH
|
||||
# Free-form chain name (e.g. "Ethereum", "Tron") — optional, not
|
||||
# validated against a fixed list (see CryptoHolding.network).
|
||||
network: str | None = Field(default=None, max_length=50)
|
||||
|
||||
|
||||
class CryptoHoldingUpdate(BaseModel):
|
||||
"""Holding-only metadata that isn't part of the buy/sell transaction
|
||||
log — currently just `network`. Quantity/price/date changes go through
|
||||
the /transactions routes instead (see add_transaction/update_transaction),
|
||||
same split as risk_level living on the Asset via PATCH /assets/{id}."""
|
||||
|
||||
network: str | None = Field(default=None, max_length=50)
|
||||
|
||||
|
||||
class CryptoHoldingRead(BaseModel):
|
||||
@@ -90,6 +102,8 @@ class CryptoHoldingRead(BaseModel):
|
||||
# same endpoint every other Asset already uses, no crypto-specific
|
||||
# update route needed.
|
||||
risk_level: RiskLevel
|
||||
# Lives on CryptoHolding itself — editable via PATCH /crypto/holdings/{asset_id}.
|
||||
network: str | None
|
||||
|
||||
# Quantity and avg_buy_price are derived from the transaction log (see
|
||||
# services/crypto_service.py's _compute_position) — never stored.
|
||||
|
||||
@@ -42,6 +42,7 @@ from app.schemas.crypto import (
|
||||
CryptoHistoryResponse,
|
||||
CryptoHoldingCreate,
|
||||
CryptoHoldingRead,
|
||||
CryptoHoldingUpdate,
|
||||
CryptoPerformancePoint,
|
||||
CryptoPerformanceResponse,
|
||||
CryptoPortfolioCreate,
|
||||
@@ -217,6 +218,7 @@ def _to_read(holding: CryptoHolding) -> CryptoHoldingRead:
|
||||
name=holding.name,
|
||||
thumb_url=holding.thumb_url,
|
||||
risk_level=holding.asset.risk_level,
|
||||
network=holding.network,
|
||||
quantity=quantity,
|
||||
avg_buy_price=avg_buy_price,
|
||||
current_price=current_price,
|
||||
@@ -442,6 +444,7 @@ async def create_holding(session: AsyncSession, payload: CryptoHoldingCreate) ->
|
||||
symbol=payload.symbol.upper(),
|
||||
name=payload.name,
|
||||
thumb_url=payload.thumb_url,
|
||||
network=payload.network,
|
||||
)
|
||||
# Wired explicitly rather than left for a lazy load off asset_id — under
|
||||
# async SQLAlchemy, a relationship access with no eager load and no prior
|
||||
@@ -488,6 +491,17 @@ async def create_holding(session: AsyncSession, payload: CryptoHoldingCreate) ->
|
||||
return _to_read(holding)
|
||||
|
||||
|
||||
async def update_holding(session: AsyncSession, asset_id: int, payload: CryptoHoldingUpdate) -> CryptoHoldingRead:
|
||||
"""Updates CryptoHolding-only metadata — currently just `network`.
|
||||
Quantity/price/date go through add_transaction/update_transaction
|
||||
instead; risk_level lives on the Asset via PATCH /assets/{asset_id}."""
|
||||
holding = await _get_holding_or_404(session, asset_id)
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(holding, field, value)
|
||||
await session.commit()
|
||||
return _to_read(holding)
|
||||
|
||||
|
||||
async def add_transaction(session: AsyncSession, asset_id: int, payload: CryptoTransactionCreate) -> CryptoHoldingRead:
|
||||
"""A buy or sell against an existing holding — never calls CoinGecko
|
||||
(see module docstring): value is recomputed from the last cached
|
||||
|
||||
Reference in New Issue
Block a user