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
+5 -7
View File
@@ -220,7 +220,6 @@ def _make_portfolio(portfolio_id: str = "p1") -> Portfolio:
status="enabled",
name="Test",
symbol="005930",
market="KRW",
tags=["tag1"],
cash_balance=1000.0,
asset_quantity=10.0,
@@ -275,7 +274,7 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None:
assert body["total"] == 1
item = body["assets"][0]
assert item["asset_id"] == "p1"
assert item["market"] == "KRW"
assert "market" not in item
assert item["broker_type"] == "kis"
assert item["cash"] == 1000.0
assert item["quantity"] == 10.0
@@ -294,7 +293,7 @@ def test_v1_assets_status_filter(monkeypatch) -> None:
p_enabled = _make_portfolio("p1") # status="enabled"
p_active = Portfolio(
portfolio_id="p2", broker="kis", status="active", name="Active",
symbol="000660", market="KRW", cash_balance=0.0, asset_quantity=0.0,
symbol="000660", cash_balance=0.0, asset_quantity=0.0,
price=0.0, profit_buffer=0.0,
)
principal = TokenPrincipal(
@@ -328,7 +327,7 @@ def test_v1_assets_strategy_params(monkeypatch) -> None:
created_at=_dt.datetime.now(_dt.timezone.utc))
p = Portfolio(
portfolio_id="p1", broker="kis", status="enabled", name="T", symbol="005930",
market="KRW", cash_balance=0.0, asset_quantity=0.0, price=0.0,
cash_balance=0.0, asset_quantity=0.0, price=0.0,
profit_buffer=0.0, active_strategy_id="ss-1",
)
principal = TokenPrincipal(
@@ -357,7 +356,7 @@ def test_v1_assets_strategy_params_fallback_on_error(monkeypatch) -> None:
p = Portfolio(
portfolio_id="p1", broker="kis", status="enabled", name="T", symbol="005930",
market="KRW", cash_balance=0.0, asset_quantity=0.0, price=0.0,
cash_balance=0.0, asset_quantity=0.0, price=0.0,
profit_buffer=0.0, active_strategy_id="ss-bad",
)
principal = TokenPrincipal(
@@ -761,7 +760,6 @@ def _make_portfolio(portfolio_id: str = "p1") -> Portfolio:
status="enabled",
name="Test",
symbol="005930",
market="KRW",
tags=["tag1"],
cash_balance=1000.0,
asset_quantity=10.0,
@@ -814,7 +812,7 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None:
body = resp.json()
assert body["total"] == 1
assert body["assets"][0]["asset_id"] == "p1"
assert body["assets"][0]["market"] == "KRW"
assert "market" not in body["assets"][0]
# ---------------------------------------------------------------------------