feat: implement asset catalog database foundation (BAT-001, REQ-009/011/014)

Asset Catalog Model Implementation (BAT-001):
- Introduce assets table with UUID PK, symbol UNIQUE, markets TEXT[] array
- Define MarketCode enum (KRX, NXT, SOR, NASDAQ, NYSE, UPBIT, BITHUMB)
- Add Asset domain model with catalog properties
- Implement asset CRUD protocols in portfolio_repository
- Add PostgreSQL asset CRUD operations with GIN index on markets

Portfolio Model Refactoring (REQ-011):
- Add portfolios.asset_id UUID FK to assets(asset_id) ON DELETE RESTRICT
- Remove portfolios.symbol and portfolios.market columns
- Update Portfolio domain model to reference asset_id instead of symbol/market
- Refactor all portfolio queries to JOIN assets catalog
- Update all repository methods to use asset references

Data Migration (REQ-014):
- Create 20260519_0017 Alembic revision for assets table creation
- Create 20260519_0018 Alembic revision for atomic data migration:
  * Extract unique symbol combinations from portfolios
  * Aggregate markets using ARRAY_AGG(DISTINCT market)
  * Populate assets catalog with extracted data
  * Update portfolios.asset_id to reference new assets
  * Remove legacy symbol/market columns in single transaction
- Implement migration with rollback support

Requirements Approved (REQ-001~008):
- REQ-001~008 transitioned from Proposed to Approved
- Complete tag model requirement set with implementation readiness

New Requirements Approved (REQ-009~015):
- REQ-009: assets table schema (approved, included in BAT-001)
- REQ-010: admin asset catalog CRUD (approved, future BAT-002)
- REQ-011: portfolios asset_id FK (approved, included in BAT-001)
- REQ-012: portfolio create catalog validation (approved, future BAT-002)
- REQ-013: GET /v1/assets token branching (approved, future BAT-002)
- REQ-014: portfolios data migration (approved, included in BAT-001)
- REQ-015: asset catalog baseline docs (approved)

Code Changes:
- app/domain/models.py: Add Asset model, MarketCode enum, Portfolio asset_id FK
- app/repositories/portfolio_repository.py: Define asset CRUD protocol
- app/repositories/postgres.py: Implement asset operations with full CRUD
- app/api/v1_assets.py: Remove symbol/market direct references
- app/cli/commands/portfolio.py: Update portfolio commands for asset references
- app/schemas/portfolio.py: Update portfolio schema with asset_id
- All portfolio operations throughout codebase: Use asset references

Testing:
- Add test_asset_catalog.py for asset CRUD operations
- Verify REQ-009 AC-1~5 (asset catalog creation, lookup, uniqueness)
- Verify REQ-011 AC-1~5 (FK constraint, portfolio asset reference)
- Verify REQ-014 AC-1~6 (migration atomicity, data correctness, rollback)
- Update existing tests for asset model usage

Documentation:
- Create BAT-001 batch documents (planning, design, implementation, verification)
- Update dcy-001 with implementation details and confirmed architecture
- Add 8 new requirement documents (REQ-009~015)
- Update docs/discovery/index.md with batch entry

Impact:
- Database: assets catalog table, portfolios refactored to reference assets
- Migration: 2 new Alembic revisions (0017 assets, 0018 portfolio migration)
- API: Symbol/market now accessed through asset references
- CLI: Portfolio commands require asset reference pattern
- All existing portfolio data preserved and correctly migrated
This commit is contained in:
2026-05-19 15:59:20 +00:00
parent 1082f041a1
commit e2226e28b6
40 changed files with 2118 additions and 168 deletions
+26 -2
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Protocol
from app.domain.models import AuthToken, BrokerCredential, Daemon, LedgerEntry, LedgerTrade, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User
from app.domain.models import Asset, AuthToken, BrokerCredential, Daemon, LedgerEntry, LedgerTrade, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User
class PortfolioRepository(Protocol):
@@ -136,7 +136,7 @@ class PortfolioRepository(Protocol):
broker: str,
status: str,
name: str,
symbol: str,
asset_id: str,
cash_balance: float,
asset_quantity: float,
asset_price: float,
@@ -145,6 +145,30 @@ class PortfolioRepository(Protocol):
avabile_strategies: list[str] | None,
) -> Portfolio: ...
def get_asset_by_id(self, asset_id: str) -> Asset | None: ...
def get_asset_by_symbol(self, symbol: str) -> Asset | None: ...
def list_assets(self) -> list[Asset]: ...
def insert_asset(
self,
*,
symbol: str,
markets: list[str],
display_name: str | None = None,
status: str = "active",
) -> Asset: ...
def update_asset(
self,
asset_id: str,
*,
markets: list[str] | None = None,
display_name: str | None = None,
status: str | None = None,
) -> Asset: ...
def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: ...
def list_portfolios(