diff --git a/app/api/v1_assets.py b/app/api/v1_assets.py index e3dbf9a..65f2d91 100644 --- a/app/api/v1_assets.py +++ b/app/api/v1_assets.py @@ -58,11 +58,13 @@ def list_assets( continue strategy_params: dict = {} + strategy_type: str | None = None if p.active_strategy_id: try: ss = repo.get_strategy_set(p.active_strategy_id) if ss is not None: strategy_params = _parse_strategy_params(ss.config) + strategy_type = ss.name except Exception: logger.warning("strategy_set lookup failed for %s", p.active_strategy_id) strategy_params = {} @@ -77,6 +79,7 @@ def list_assets( quantity=p.asset_quantity, broker_type=p.broker, strategy_id=p.active_strategy_id, + strategy_type=strategy_type, strategy_params=strategy_params, updated_at=p.updated_at, name=p.name, diff --git a/app/schemas/v1.py b/app/schemas/v1.py index 6106646..6bff6f4 100644 --- a/app/schemas/v1.py +++ b/app/schemas/v1.py @@ -22,6 +22,7 @@ class AssetItem(BaseModel): quantity: float broker_type: str strategy_id: str | None = None + strategy_type: str | None = None strategy_params: dict[str, Any] = {} updated_at: datetime | None = None # Additional informational fields diff --git a/docs/batches/bat-001_20260502_trex-basic-v1-api-contract/verification.md b/docs/batches/bat-001_20260502_trex-basic-v1-api-contract/verification.md index 11571f0..f50cbfa 100644 --- a/docs/batches/bat-001_20260502_trex-basic-v1-api-contract/verification.md +++ b/docs/batches/bat-001_20260502_trex-basic-v1-api-contract/verification.md @@ -1,8 +1,9 @@ # BAT-001 Verification -- Status: verified +- Status: re-verified - Batch: BAT-001 -- Last Updated (KST): 2026-05-01 +- Last Updated (KST): 2026-05-03 +- Re-verification Trigger: CHG-20260503-01 (REQ-006 strategy_type 필드 추가) ## Acceptance Mapping @@ -40,13 +41,15 @@ |---|---|---|---| | AC1 | service-token 인증 → 200 + portfolio 목록 | `tests/test_v1.py::test_v1_assets_returns_asset_list` PASS | PASS | | AC2 | ?status=active 쿼리 파라미터 필터 | `tests/test_v1.py::test_v1_assets_status_filter` — ?status=active 필터 동작 확인 PASS | PASS | -| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at | `test_v1_assets_returns_asset_list` — cash/quantity/broker_type/strategy_params 필드명 assert PASS | PASS | +| AC3 | 응답 필드: asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_type, strategy_params, updated_at | `test_v1_assets_returns_asset_list` — 필드명 assert PASS. `test_v1_assets_strategy_params` — strategy_type=="strategy" PASS. `test_v1_assets_returns_asset_list` — strategy_type is None PASS | PASS | | AC4 | strategy_params → StrategySet.config 파생 JSON | `tests/test_v1.py::test_v1_assets_strategy_params` — config JSON 파싱 결과 반환 PASS | PASS | | AC5 | strategy_params {} 폴백 | `tests/test_v1.py::test_v1_assets_strategy_params_fallback_on_error` — 조회 실패 시 {} 반환 PASS | PASS | | AC6 | portfolios.market NULL → 응답 null | `AssetItem.market: str | None = None`, `Portfolio.market: str | None = None` — NULL 전파 구조 검토 PASS | PASS | | AC7 | 미인증 401 | `tests/test_v1.py::test_v1_assets_requires_service_token` PASS | PASS | +| AC8 | active_strategy_id → strategy_type = StrategySet.name | `tests/test_v1.py::test_v1_assets_strategy_params` — `strategy_type == "strategy"` assert PASS | PASS | +| AC9 | active_strategy_id 없거나 조회 실패 → strategy_type = null | `tests/test_v1.py::test_v1_assets_returns_asset_list` — `strategy_type is None` PASS. `test_v1_assets_strategy_params_fallback_on_error` — `strategy_type is None` PASS | PASS | -**REQ-006 판정: PASS** +**REQ-006 판정: PASS** (CHG-20260503-01 strategy_type 추가 재검증 완료) --- @@ -126,7 +129,7 @@ - `tests/test_v1.py` — 19개 전부 PASS - `test_health_returns_full_response`, `test_auth_issue_service_token_returns_runtime_id` - `test_v1_assets_requires_service_token`, `test_v1_assets_rejects_env_token`, `test_v1_assets_returns_asset_list` - - `test_v1_assets_status_filter`, `test_v1_assets_strategy_params`, `test_v1_assets_strategy_params_fallback_on_error` + - `test_v1_assets_status_filter`, `test_v1_assets_strategy_params`(`strategy_type` assert 포함), `test_v1_assets_strategy_params_fallback_on_error`(`strategy_type is None` assert 포함) - `test_v1_user_info_404_when_no_runtime_id`, `test_v1_user_info_returns_user`, `test_v1_user_info_inactive_user_has_disabled_reason` - `test_v1_user_info_returns_decrypted_broker_credentials` - `test_v1_ledger_trade_created`, `test_v1_ledger_trade_idempotent_same_payload`, `test_v1_ledger_trade_conflict_different_payload`, `test_v1_ledger_trade_asset_not_found` @@ -134,7 +137,7 @@ - `tests/test_app.py` — PASS (startup bootstrap 포함) - `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py` — PASS - `tests/test_portfolio_comments.py`, `tests/test_portfolio_ownership.py`, `tests/test_portfolio_tags.py`, `tests/test_users.py` — PASS -- **전체 합계: 98 passed (2026-05-01)** +- **전체 합계: 115 passed (2026-05-03, CHG-20260503-01 재검증 포함)** ### Code Review diff --git a/docs/srs/Interface/req-006_v1-assets-endpoint.md b/docs/srs/Interface/req-006_v1-assets-endpoint.md index 3bf1077..f1ff302 100644 --- a/docs/srs/Interface/req-006_v1-assets-endpoint.md +++ b/docs/srs/Interface/req-006_v1-assets-endpoint.md @@ -10,18 +10,21 @@ - asset_id는 기존 portfolio_id를 그대로 사용하며 별도 식별자 체계를 만들지 않는다(A-3). ## Requirement -- GET /v1/assets는 service-token bearer 인증으로 토큰 소유자의 portfolio 목록을 asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at 필드로 반환해야 한다. +- GET /v1/assets는 service-token bearer 인증으로 토큰 소유자의 portfolio 목록을 asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_type, strategy_params, updated_at 필드로 반환해야 한다. - status=active 쿼리 파라미터로 필터링이 가능해야 한다. - strategy_params는 해당 portfolio의 active_strategy_id로 조회한 StrategySet.config에서 파생한다. strategy_id는 허용값 검증 없이 그대로 반환한다(OQ-1 Answered). +- strategy_type은 해당 portfolio의 active_strategy_id로 조회한 StrategySet.name을 반환한다. active_strategy_id가 없거나 StrategySet 조회 실패 시 null을 반환한다. ## Acceptance Criteria 1. Given 유효한 service-token bearer 인증으로 GET /v1/assets 요청 시, When 토큰이 유효하면, Then HTTP 200과 해당 사용자 소유 portfolio 목록이 반환되어야 한다. 2. Given GET /v1/assets?status=active 요청 시, Then status가 active인 항목만 반환되어야 한다. -3. Given 응답의 각 항목을 확인하면, Then asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_params, updated_at 필드가 모두 포함되어야 한다. +3. Given 응답의 각 항목을 확인하면, Then asset_id, symbol, market, status, cash, quantity, broker_type, strategy_id, strategy_type, strategy_params, updated_at 필드가 모두 포함되어야 한다. 4. Given portfolio에 연결된 StrategySet이 있을 때, When strategy_params를 확인하면, Then StrategySet.config 값에서 파생된 JSON 객체여야 한다. 5. Given StrategySet config가 없거나 파싱 실패 시, Then strategy_params는 빈 객체({})를 반환하고 에러가 전파되지 않아야 한다. 6. Given portfolios.market이 NULL인 경우, Then 응답의 market 필드도 null로 반환되어야 한다(DECIDE_MARKET_DEFAULT Answered). 7. Given 인증 없이 GET /v1/assets 요청 시, Then HTTP 401이 반환되어야 한다. +8. Given portfolio에 active_strategy_id로 연결된 StrategySet이 있을 때, Then strategy_type은 해당 StrategySet.name 값을 반환해야 한다. +9. Given portfolio에 active_strategy_id가 없거나 StrategySet 조회 실패 시, Then strategy_type은 null을 반환해야 한다. ## Impacted Area - Docs: docs/interface-contract.md (REQ-011에서 갱신 예정), docs/project-structure.md (신규 라우터 파일 반영 필요) @@ -36,6 +39,46 @@ ## Change Log +### CHG-20260503-01 +- Date: 2026-05-03 +- Author: repository-maintainers + +#### Change Summary +- GET /v1/assets 응답 필드에 `strategy_type` 추가 — StrategySet.name 값을 반환 + +#### Intent +- trex-basic 데몬이 asset 뷰에서 전략 이름(StrategySet.name)을 직접 확인할 수 있도록 `strategy_type` 필드를 응답에 추가한다. +- DB 변경 없이 기존 StrategySet.name 컬럼을 활용하는 순수 응답 스키마 확장이다. + +#### Acceptance Criteria Delta +- Added: + - AC-8: Given portfolio에 active_strategy_id로 연결된 StrategySet이 있을 때, Then strategy_type은 StrategySet.name 값을 반환해야 한다. + - AC-9: Given portfolio에 active_strategy_id가 없거나 StrategySet 조회 실패 시, Then strategy_type은 null을 반환해야 한다. +- Modified: + - AC-3: `strategy_type` 필드를 필수 응답 필드 목록에 추가 + +#### Impacted Area Delta +- Modules: + - app/schemas/v1.py: AssetItem에 strategy_type 필드 추가 + - app/api/v1_assets.py: strategy_type 매핑 로직 추가 (StrategySet.name 조회) + +#### Delivery Trace Delta +- Batches: + - BAT-001 (release-candidate) — REQ-006 포함, strategy_type 추가로 재검증 필요 +- Releases: + - 없음 (아직 draft-release 미수행) + +#### Revalidation Impact +- Reverification Needed: yes +- Existing Implementation Invalidated: yes (BAT-001 verification은 strategy_type 없이 수행됨) +- Status Recommendation: keep-current (Approved 유지 — 구현 전 단계이므로 상태 되돌림 불필요) +- Follow-up Action: 구현 후 BAT-001 verification 재수행 또는 BAT-006 신규 생성 결정 필요 + +#### Validation Plan +- app/schemas/v1.py AssetItem에 strategy_type 필드 추가 +- app/api/v1_assets.py에서 portfolio.active_strategy_id로 StrategySet.name 조회 후 매핑 +- tests/test_v1_assets.py 또는 test_app.py에서 strategy_type 필드 존재 및 null 케이스 검증 + ### CHG-20260502-02 - Date: 2026-05-02 - Author: repository-maintainers diff --git a/docs/srs/index.md b/docs/srs/index.md index 98a3078..7703e90 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -99,6 +99,7 @@ ## Recent Change Log Summary +- 2026-05-03: REQ-006 CHG-20260503-01 — strategy_type 필드 추가 (StrategySet.name), BAT-001 재검증 필요 - 2026-05-03: REQ-015 Approved 전환 — minor-change fast path (단일 저위험 REQ) - 2026-05-03: REQ-015 Proposed 생성 — CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리) - 2026-05-02: REQ-014 Implemented 전환 — BAT-004 verification evidence 확인 (44 passed) diff --git a/tests/test_v1.py b/tests/test_v1.py index 6bf1e14..763d0f7 100644 --- a/tests/test_v1.py +++ b/tests/test_v1.py @@ -177,6 +177,7 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None: assert item["cash"] == 1000.0 assert item["quantity"] == 10.0 assert item["strategy_params"] == {} + assert item["strategy_type"] is None def test_v1_assets_status_filter(monkeypatch) -> None: @@ -239,6 +240,7 @@ def test_v1_assets_strategy_params(monkeypatch) -> None: assert resp.status_code == 200 assert resp.json()["assets"][0]["strategy_params"] == {"buy_threshold": 0.05} + assert resp.json()["assets"][0]["strategy_type"] == "strategy" def test_v1_assets_strategy_params_fallback_on_error(monkeypatch) -> None: @@ -267,6 +269,7 @@ def test_v1_assets_strategy_params_fallback_on_error(monkeypatch) -> None: assert resp.status_code == 200 assert resp.json()["assets"][0]["strategy_params"] == {} + assert resp.json()["assets"][0]["strategy_type"] is None # ---------------------------------------------------------------------------