mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-12 18:46:40 +07:00
feat: добавить уровень риска для крипто-холдингов
Риск (низкий/средний/высокий) задаётся при добавлении монеты и редактируется прямо в таблице холдингов. Добавлена вкладка "Уровни риска" в карточке портфеля с разбивкой % по риск-тирам, только для крипты. Заодно поправлено округление процентов в риск-карточках.
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.models.enums import CryptoTransactionType
|
||||
from app.models.enums import CryptoTransactionType, RiskLevel
|
||||
|
||||
|
||||
class CryptoTransactionCreate(BaseModel):
|
||||
@@ -72,6 +72,10 @@ class CryptoHoldingCreate(BaseModel):
|
||||
price_per_unit: Decimal = Field(gt=0, max_digits=38, decimal_places=18)
|
||||
date: date_ = Field(default_factory=date_.today)
|
||||
note: str | None = Field(default=None, max_length=500)
|
||||
# Defaults to HIGH — crypto is this app's textbook HIGH example (see
|
||||
# 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
|
||||
|
||||
|
||||
class CryptoHoldingRead(BaseModel):
|
||||
@@ -81,6 +85,11 @@ class CryptoHoldingRead(BaseModel):
|
||||
symbol: str
|
||||
name: str
|
||||
thumb_url: str | None
|
||||
# Lives on the underlying Asset (see services/crypto_service.py's
|
||||
# _to_read) — editable via the existing PATCH /assets/{asset_id},
|
||||
# same endpoint every other Asset already uses, no crypto-specific
|
||||
# update route needed.
|
||||
risk_level: RiskLevel
|
||||
|
||||
# Quantity and avg_buy_price are derived from the transaction log (see
|
||||
# services/crypto_service.py's _compute_position) — never stored.
|
||||
|
||||
@@ -36,7 +36,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.core.config import get_settings
|
||||
from app.models.asset import Asset, AssetValuation
|
||||
from app.models.crypto import CryptoHolding, CryptoPortfolio, CryptoSyncState, CryptoTransaction
|
||||
from app.models.enums import AssetClass, CapitalRole, CryptoTransactionType, RiskLevel
|
||||
from app.models.enums import AssetClass, CapitalRole, CryptoTransactionType
|
||||
from app.schemas.crypto import (
|
||||
CryptoHistoryPoint,
|
||||
CryptoHistoryResponse,
|
||||
@@ -82,7 +82,7 @@ AUTO_REFRESH_INTERVAL = timedelta(hours=24)
|
||||
# two points, not the smooth intraday line a denser data source could draw.
|
||||
CRYPTO_RANGE_DAYS: dict[str, int] = {"7d": 7, "30d": 30, "90d": 90}
|
||||
|
||||
_EAGER = (selectinload(CryptoHolding.transactions),)
|
||||
_EAGER = (selectinload(CryptoHolding.transactions), selectinload(CryptoHolding.asset))
|
||||
|
||||
|
||||
def _require_api_key() -> str:
|
||||
@@ -216,6 +216,7 @@ def _to_read(holding: CryptoHolding) -> CryptoHoldingRead:
|
||||
symbol=holding.symbol,
|
||||
name=holding.name,
|
||||
thumb_url=holding.thumb_url,
|
||||
risk_level=holding.asset.risk_level,
|
||||
quantity=quantity,
|
||||
avg_buy_price=avg_buy_price,
|
||||
current_price=current_price,
|
||||
@@ -424,10 +425,12 @@ async def create_holding(session: AsyncSession, payload: CryptoHoldingCreate) ->
|
||||
asset_class=AssetClass.CRYPTO,
|
||||
currency=settings.currency,
|
||||
capital_role=CapitalRole.NEUTRAL,
|
||||
# Defaults to HIGH, not the Asset model's own MEDIUM default — matches
|
||||
# this app's own risk-level copy, which names crypto as the textbook
|
||||
# HIGH example (see lib/i18n.ts's netWorth.riskLevelFormHint.high).
|
||||
risk_level=RiskLevel.HIGH,
|
||||
# User-chosen at add time (see CryptoHoldingCreate), defaulting to
|
||||
# HIGH — not the Asset model's own MEDIUM default — since this app's
|
||||
# own risk-level copy names crypto as the textbook HIGH example (see
|
||||
# lib/i18n.ts's netWorth.riskLevelFormHint.high). Editable later via
|
||||
# PATCH /assets/{asset_id}, same as any other Asset.
|
||||
risk_level=payload.risk_level,
|
||||
)
|
||||
session.add(asset)
|
||||
await session.flush()
|
||||
@@ -440,6 +443,11 @@ async def create_holding(session: AsyncSession, payload: CryptoHoldingCreate) ->
|
||||
name=payload.name,
|
||||
thumb_url=payload.thumb_url,
|
||||
)
|
||||
# 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
|
||||
# assignment raises MissingGreenlet, and _to_read (called at the end of
|
||||
# this function) reads holding.asset.risk_level.
|
||||
holding.asset = asset
|
||||
session.add(holding)
|
||||
|
||||
holding.transactions.append(
|
||||
|
||||
Reference in New Issue
Block a user