feat: add portfolio snapshot read layer

This commit is contained in:
2026-06-06 09:33:00 +09:00
parent 56e977a7ad
commit ca757009f5
51 changed files with 3565 additions and 51 deletions
@@ -0,0 +1,62 @@
# BAT-008 Design
- Status: draft
- Batch: BAT-008
## Architecture Summary
- 신규 `trex_core/adapters/portfolio/` read 계층을 추가해 portfolio-hub universe 를 가져온다. 스냅샷은 in-memory store 에 유지하되, pause overlay 만 Postgres 에 영속한다. warning 은 자동·임시 overlay 로 in-memory 에만 유지하고 다음 정합 성공 시 자동 해제한다. daemon 에 스케줄러 스레드를 추가해 일일 refresh 를 수행하고, BAT-007 의 snapshot RPC consumer 핸들러를 확장해 선별·마스킹·envelope 산출 로직을 연결한다.
## Changed Areas
- Docs: docs/interface-contract.md, docs/runtime-flows.md, docs/project-structure.md, docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md
- Modules:
- `trex_core/application/portfolio.py` (신규): `PortfolioHubReadPort` Protocol + 스냅샷 선별 서비스 + masking 합성 로직
- `trex_core/adapters/portfolio/` (신규): `hub_read.py`(실 어댑터), `stub_read.py`(결정론 stub)
- `trex_core/adapters/portfolio/pause_store.py` (신규): Postgres `snapshot_pause` 테이블 CRUD
- `trex_core/adapters/messaging/snapshot_handler.py` (확장): snapshot RPC 핸들러에 선별/envelope/degraded 로직 연결
- `trex_core/runtime/daemon.py` (확장): `_snapshot_scheduler` 스레드 추가, pause/resume RPC 제어 핸들러 연결
- `trex_core/runtime/config.py` (확장): `TREX_SNAPSHOT_SCHEDULE_KST` env, daemon_id 토큰 도출·캐시
- `trex_core/adapters/broker/` (확장): `BrokerPort.get_balance()` 메서드 추가 (KIS + paper stub)
- Interfaces:
- RabbitMQ `trex.snapshot.req` RPC: `{strategy_key?: str, view?: "all"|"buyable"|"held"}` → `{assets: [], view: str, available_slots: int, degraded: bool, reason: str|null}`
- CLI: `trex-core pause <portfolio_id>` / `trex-core resume <portfolio_id>`
- RPC 제어 메시지 (BAT-007 채널): `{action: "pause"|"resume", portfolio_id: str}` 핸들러
## Key Decisions
1. **스냅샷 store 분리**: in-memory `SnapshotStore`(universe·현금·포지션·warning) + Postgres `snapshot_pause`(pause). warning 은 process 재시작 시 초기화(일시적·자동), pause 는 Postgres 에서 복원(영속·수동). 이 분리가 REQ-031/032 의 "자동 warning vs 수동 pause" 시맨틱을 명확히 구현한다.
2. **masking 합성 순서**: `pending(slot 상태) OR warning(정합 실패) OR pause(수동)` → 셋 중 하나라도 true 면 마스킹. 적용은 view 집합 산출 후 균일 적용. 우선순위: 슬롯 overlay > portfolio-hub 보유 여부.
3. **daemon_id 도출**: `PortfolioHubReadPort` 초기화 시 portfolio-hub `GET /v1/auth/whoami` 1회 호출 → `daemon_id` 추출·캐시. `TREX_PORTFOLIO_HUB_TOKEN` 자격증명 재사용(REQ-027 A-1).
4. **strategy_key 필터**: 요청 payload 의 `strategy_key`(str) 를 자산의 `active_strategy_id` 와 **대소문자 구분 정확 일치**(`==`)로 필터링. 생략 시 필터 없음.
5. **degraded 분기**:
- `portfolio_hub_unavailable`: 스케줄러 fetch 또는 RPC handler 시점에 portfolio-hub 에 연결 불가.
- `snapshot_fetch_failed`: fetch 시도 자체는 성공했으나 응답 파싱/검증 실패.
- `cash_position_stale`: 옵션 D 갱신 실패(ledger write 응답에 `*_after` 없거나 stale 감지).
- 각 경우 sanitized message 포함, raw exception/credentials 노출 금지.
6. **스케줄러 스레드 snapshot 교체**: 일일 fetch 완료 시 `SnapshotStore` 를 **immutable swap**(새 dict 할당 후 atomic reference 교체) 으로 교체. snapshot RPC handler 가 구버전·신버전 혼합 읽기 없도록 보장. warning/pause overlay 는 swap 후 재적용.
7. **KIS D-2 0수량 잔고 처리**: `hldg_qty == 0` 인 잔고 항목은 **미보유**로 취급(잔고 없음). 트리거: 당일 전량매도 후 KIS 잔고가 D-2 까지 0으로 남는 케이스. 이 항목이 symbol 집계 결과 0수량이면 portfolio-hub 포지션 0과 일치(warning 없음).
8. **BrokerPort 잔고조회 확장**: `get_balance() → list[BalanceItem]` 메서드를 `BrokerPort` Protocol 에 추가. KIS 어댑터: `TTTC8434R`(실전)/`VTTC8434R`(모의) 분기. paper stub: 결정론 고정 잔고 반환. 기존 REQ-011/012 어댑터에 메서드 추가(인터페이스 확장, 기존 동작 불변).
9. **옵션 D 연결점**: ledger write 응답(BAT-006 `LedgerAdapter`) 이 `cash_balance_after`/`asset_quantity_after` 를 반환하면 `SnapshotStore.update_cash_position(asset_id, cash, qty)` 호출. portfolio-hub#8 배포 전 응답에 해당 필드가 없으면 갱신 skip(graceful).
## Edge Cases
- portfolio-hub 불가 **at startup**: `SnapshotStore` 비어 있음 + `degraded=true`. 전략 요청에 `assets: [], degraded: true, reason: "portfolio_hub_unavailable"` 반환. 신규 매매 보류(전략 모듈 책임).
- `strategy_key` 필터 결과가 빈 배열: `assets: [], degraded: false`. 필터 미매치는 degraded 가 아님.
- `available_slots=0` + `view=buyable`: `assets: [], available_slots: 0, degraded: false`.
- 같은 자산에 warning + pause 동시: 둘 다 마스킹 적용(OR), 어느 쪽이 먼저 해제되든 나머지 하나가 남아있으면 여전히 마스킹.
- pause 중 일일 refresh: universe·포지션 갱신 후 pause overlay 재적용(pause 불변).
- holding 슬롯 자산의 수량 표기: snapshot baseline 값 사용. holding 중 ledger flush 지연으로 실 포지션과 갭이 있을 수 있음 — 옵션 D 갱신이 완료되면 write 응답으로 즉시 보정.
## Architecture Impact
- both (project-structure: `adapters/portfolio/` 신규 계층, runtime-flows: 스케줄러 스레드 + 일일 fetch + masking 합성 흐름 + 옵션 D ledger hook)
## Reference Doc Update Plan
- project-structure.md: `adapters/portfolio/`(read 어댑터·stub·pause store) 신규 섹션, `application/portfolio.py` 포트·선별 서비스, 스케줄러 스레드 추가.
- runtime-flows.md: 일일 스냅샷 fetch 흐름, 정합 검증 흐름, pause/resume 흐름, snapshot RPC 응답 산출 흐름(masking 합성 포함), 옵션 D ledger write hook 흐름.
- interface-contract.md: `trex.snapshot.req` RPC 요청/응답 envelope 계약, pause/resume 제어 메시지 계약, `PortfolioHubReadPort` 포트 시그니처.
@@ -0,0 +1,69 @@
# BAT-008 Implementation
- Status: complete (M1-M8; M9 옵션 D 는 portfolio-hub#8 배포 후)
- Batch: BAT-008
## Plan Summary
- M1-M8 마일스톤 모두 완료. M9(REQ-030 옵션 D `*_after` 갱신)는 portfolio-hub#8 배포 후 별도 실행. `SnapshotStore.update_cash_position` wiring point 가 이미 노출되어 있어 배포 후 LedgerWriter 호출 1줄 추가로 활성화 가능.
## Changed Files
- 신규
- `trex_core/application/portfolio.py` — `PortfolioHubReadPort` Protocol, `PortfolioItem`, `PortfolioHubUnavailableError`, `View`/`DegradedReason` enum, `SnapshotStore`(swap/warning/option-D/degraded), `PauseStore` Protocol + `InMemoryPauseStore`, `SnapshotEnvelope` + `build_snapshot_envelope`.
- `trex_core/application/reconciliation.py` — `reconcile_positions(store, broker)` symbol-aggregate 비교 + warning 자동 set/clear.
- `trex_core/application/snapshot_scheduler.py` — `SnapshotScheduler.run_once()` (fetch + swap + reconcile) + `run_forever(stop, tick_sec)` (boot 즉시 1회 + KST 시각 도래 시 1회).
- `trex_core/adapters/portfolio/__init__.py`, `hub_read.py`(`PortfolioHubReadAdapter` httpx — whoami + list_portfolios), `stub_read.py`(`StubPortfolioRead` 결정론), `pause_store.py`(`PostgreSQLPauseStore` upsert/delete).
- `trex_core/adapters/persistence/sql/003_create_snapshot_pause.sql` — `snapshot_pause(portfolio_id PK, actor, created_at, updated_at)`.
- `trex_core/runtime/cli.py` + `trex-core` console_script — `pause/resume/paused` 서브커맨드.
- 테스트: `tests/adapters/portfolio/{test_stub_read,test_hub_read,test_postgresql_pause_store}.py`, `tests/application/{test_snapshot_selection,test_snapshot_handler,test_reconciliation,test_snapshot_scheduler,test_pause_handler}.py`.
- 수정
- `trex_core/application/broker_port.py` — `BalanceItem` 데이터클래스 + `BrokerPort.get_balance() -> list[BalanceItem]` 메서드 추가.
- `trex_core/adapters/broker/paper.py` — `get_balance()`: fill aggregate (buy − sell) per symbol, 0수량 drop.
- `trex_core/adapters/broker/kis.py` — `get_balance()`: `TTTC8434R`/`VTTC8434R` `inquire-balance` 호출, `output1[*].pdno + hldg_qty`, 0수량 drop (D-2 leftover 처리, Decision 7).
- `trex_core/adapters/messaging/topology.py` — `PAUSE_REQ_QUEUE = "trex.pause.req"` 추가.
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology()` 가 `PAUSE_REQ_QUEUE` 도 선언.
- `trex_core/application/messaging_handlers.py` — `make_snapshot_rpc_handler` 에 `snapshot_store`/`pause_store`/`default_total_slots` 인자 추가 (BAT-008 envelope path). 인자 미제공 시 BAT-007 stub-shape 폴백. 신규 `make_pause_rpc_handler(pause_store)`.
- `trex_core/runtime/config.py` — `SnapshotScheduleConfig` + `TREX_SNAPSHOT_SCHEDULE_KST`/`TREX_SNAPSHOT_TICK_SEC` env 로딩, `DaemonConfig.snapshot_schedule` 필드.
- `trex_core/runtime/daemon.py` — `_build_portfolio_read(config)`, `_parse_kst_time`. `Daemon` 에 `snapshot_store`/`pause_store`/`portfolio_read`/`snapshot_scheduler` 인스턴스 추가. 신규 worker thread `_pause_rpc_loop`, `_snapshot_scheduler_loop`. `_snapshot_rpc_loop` 가 BAT-008 envelope handler 사용. `start()` 의 worker 목록을 6개로 확장.
- `pyproject.toml` — `trex-core = "trex_core.runtime.cli:main"` console_script 추가.
- `.env.example` — `TREX_SNAPSHOT_SCHEDULE_KST=07:30`, `TREX_SNAPSHOT_TICK_SEC=60` 추가.
- `tests/conftest.py` — `_PG_TABLES` 에 `snapshot_pause` 추가.
- `tests/adapters/persistence/test_migration_and_drift.py` — `_EXPECTED_TABLES` 에 `snapshot_pause` 추가.
- baseline 문서 갱신 — `docs/runtime-flows.md`/`docs/interface-contract.md`/`docs/project-structure.md` 에 BAT-008 섹션 + Change Log 추가.
## Execution Log
1. baseline 테스트 147 passed (PG + RabbitMQ 게이트 모두 가용) 확인.
2. M1 (REQ-027): `application/portfolio.py` 의 read 포트/DTO/store + `adapters/portfolio/{hub_read,stub_read}` + `pause_store.py` 작성. 단위테스트 + httpx MockTransport 기반 어댑터 contract 테스트 작성.
3. M2 (REQ-030 baseline): `snapshot_scheduler.py` 의 `SnapshotScheduler` + `run_once()` 작성. `runtime/config.py` 에 `SnapshotScheduleConfig` 추가, `runtime/daemon.py` 에 `_snapshot_scheduler_loop` 스레드 추가.
4. M3 (REQ-031): `BrokerPort.get_balance()` 확장 + paper/KIS 구현 + `application/reconciliation.py` 의 `reconcile_positions` 작성 + 단위테스트.
5. M4 (REQ-032): `003_create_snapshot_pause.sql` 마이그레이션 + `PostgreSQLPauseStore` + `make_pause_rpc_handler` + `runtime/cli.py` 작성. RPC handler / PG persistence / CLI smoke 모두 테스트.
6. M5 (REQ-029): `build_snapshot_envelope` 의 base(view) 산출 + masking OR 합성 + strategy_key 필터 작성 + 10개 시나리오 테스트.
7. M6 (REQ-028): `make_snapshot_rpc_handler` 를 envelope path 로 확장 (legacy stub-shape 폴백 유지 → BAT-007 회귀 0). degraded 신호 sanitization 포함.
8. M7 통합 검증: stub `SnapshotStore` + slot 도메인 + warning + pause 합성 시퀀스 테스트 + 도메인 import 스윕 회귀 통과.
9. M8 (REQ-033): baseline 3 문서(runtime-flows / interface-contract / project-structure) 에 BAT-008 섹션 + Change Log 추가.
10. End-to-end 운영 환경 검증: 실 PostgreSQL + 실 RabbitMQ 위에서 `trex-core-daemon` 기동 → snapshot RPC (view=all/buyable/held + strategy_key 필터) → pause/resume RPC → snapshot 재요청까지 1회 통과. portfolio-hub 서버 미가동 환경에서 `degraded=true reason=portfolio_hub_unavailable` 정상 반환 확인.
11. 최종 검증: `TREX_RABBITMQ_TEST_URL` + `TREX_POSTGRESQL_TEST_DSN` 게이트 모두 가용 상태에서 `pytest -q` → **185 passed / 1 skipped** (KIS sandbox 토큰 rate-limit 1건만 skip). `ruff check trex_core tests` → All checks passed.
## Validation
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" TREX_POSTGRESQL_TEST_DSN=$(grep TREX_POSTGRESQL_DSN .env | cut -d= -f2-) .venv/bin/python -m pytest -q`
- 결과: `185 passed, 1 skipped` (KIS sandbox 토큰 rate-limit).
- 명령: `.venv/bin/ruff check trex_core tests`
- 결과: `All checks passed!`.
- 추가 테스트 ~39 종:
- `tests/adapters/portfolio/test_stub_read.py` — 3 (REQ-027 AC-2)
- `tests/adapters/portfolio/test_hub_read.py` — 4 (REQ-027 AC-1/AC-4)
- `tests/adapters/portfolio/test_postgresql_pause_store.py` — 4 (REQ-032 AC-2)
- `tests/application/test_snapshot_selection.py` — 10 (REQ-029 AC-1~5, REQ-028 AC-1/AC-2/AC-3)
- `tests/application/test_snapshot_handler.py` — 4 (REQ-028)
- `tests/application/test_reconciliation.py` — 4 (REQ-031 AC-1/AC-2/AC-3)
- `tests/application/test_snapshot_scheduler.py` — 4 (REQ-030 AC-1, 옵션 D wiring point)
- `tests/application/test_pause_handler.py` — 5 (REQ-032 AC-1/AC-3/AC-4)
- 운영 환경 검증 (실 PG + RabbitMQ + `trex-core-daemon`):
- snapshot RPC envelope 모든 키 + no `price` 필드 + `degraded` 신호 확인.
- pause RPC → snapshot 마스킹 → resume RPC → snapshot 복귀 확인.
- view 3 모드 + strategy_key 필터가 envelope 응답에 반영됨.
## Remaining Risks
- **REQ-030 옵션 D (AC-2/AC-3)**: portfolio-hub#8 배포 대기. `SnapshotStore.update_cash_position` wiring point 는 이미 노출됨 — 배포 확인 후 LedgerWriter 의 ledger write 응답 소비 경로에서 `cash_balance_after`/`asset_quantity_after` 가 있을 때 호출 1줄 추가로 활성. 본 batch verification 에서는 deferred residual risk 로 기록.
- **KIS D-2 0수량 잔고 운영 검증**: 설계 Decision 7(0수량 drop) 은 paper broker 테스트로 회귀 가드되어 있으나, 실 KIS 환경에서 D-2 leftover 케이스를 한 번 관찰해 운영 노트화 권장.
- **scheduler 실 운영 검증**: 본 batch 의 검증은 `run_once()` 단위테스트 + boot 즉시 fire 운영 확인까지. `TREX_SNAPSHOT_SCHEDULE_KST` 시각 fire 는 실 데몬 운영 후 첫 07:30 KST 도래 시 관찰 필요.
- **portfolio-hub HTTP 통합 evidence**: `httpx.MockTransport` contract 테스트 + 실 데몬이 portfolio-hub 부재 시 `degraded` 정상 노출까지 검증. portfolio-hub 가용 환경에서 실 응답 round-trip 은 운영 단계 추가 evidence 로 보강 권장.
@@ -0,0 +1,39 @@
# BAT-008: portfolio-hub 스냅샷 read 레이어
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-005
- Included REQ: REQ-027, REQ-028, REQ-029, REQ-030, REQ-031, REQ-032, REQ-033
- Excluded REQ: 없음
## Summary
- dcy-005 스냅샷 read 슬라이스 전체를 하나의 delivery 단위로 묶는다. portfolio-hub read 포트/어댑터로 universe 를 가져오고(REQ-027), 일일 스케줄러로 스냅샷을 갱신하며(REQ-030), KIS 잔고조회로 포지션 정합을 검증해 warning 마스킹을 적용하고(REQ-031), 수동 pause/resume 제어를 Postgres 에 영속한다(REQ-032). 이 모든 overlay 를 합성해 slot 상태 기반 자산 선별·view·strategy_key 필터링으로 응답 집합을 산출하고(REQ-029), BAT-007 의 RabbitMQ snapshot RPC 채널로 전달하는 메시지 계약과 degraded 신호를 정의한다(REQ-028). 구현 완료 후 baseline 3문서를 정합한다(REQ-033).
## Scope
- In Scope:
- REQ-027: `PortfolioHubReadPort.get_portfolios(daemon_id)` 포트 + portfolio-hub read 어댑터(`managed_by_daemon_id` 스코프, `list_portfolios_v1`) + 결정론 stub. `daemon_id` 는 portfolio-hub 인증 토큰에서 도출·캐시(별도 `TREX_DAEMON_ID` 불필요). 각 자산: symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params(opaque). 도메인 순수성 import 스윕(NFR-1).
- REQ-028: snapshot RPC(`trex.snapshot.req`) 메시지 계약 — 요청(선택적 strategy_key·view), envelope 응답(`assets[]`·view·available_slots·degraded·reason). portfolio-hub 불가 시 `degraded=true` + machine-readable reason code + sanitized message(raw 예외 노출 금지).
- REQ-029: 응답 base 집합은 view(`all`/`buyable`/`held`)로 선택 후 마스킹(pending·warning·pause) 균일 적용. strategy_key 직교 필터. 최종 합성: `base(view) − mask(pending·warning·pause) − strategy_key 필터`. 슬롯 overlay 가 portfolio-hub 보유 여부보다 우선.
- REQ-030: 설정 가능한 시각(기본 07:30 KST)에 스케줄러가 universe 를 페치해 스냅샷(멤버십·전략파라미터·baseline 현금/포지션) 갱신. 체결 후 ledger write 응답의 `cash_balance_after`/`asset_quantity_after`(옵션 D)로 현금/포지션 갱신 — portfolio-hub#8 배포 후 착수.
- REQ-031: 스냅샷 페치 시 KIS 주식잔고조회(`TTTC8434R`/`VTTC8434R`) 수량 정확 일치 검증(symbol 집계 vs broker `hldg_qty`). 불일치 자산 warning 마스킹 + 다음 정합 성공 시 자동 해제.
- REQ-032: pause 단위 portfolio_id, CLI + dcy-006 RPC 제어 메시지 양방향. Postgres 영속(재시작/refresh 후에도 유지). 전역 마스킹, resume 시 다음 요청부터 즉시 복귀.
- REQ-033: `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 를 read 포트·스냅샷 계약·스케줄러·정합 검증 흐름에 맞게 갱신.
- Out of Scope:
- portfolio-hub write(ledger) 연동 — BAT-006(REQ-017~019) 소관; 본 batch 는 read 만.
- RabbitMQ transport 토폴로지/delivery 신뢰성 — BAT-007(REQ-021~026) 소관.
- 전략 모듈 측 RabbitMQ 클라이언트 구현(외부).
- REQ-030 옵션 D 현금/포지션 갱신 구현 — portfolio-hub#8 배포 전 대기.
- 강제 청산(kill-switch) — trex-core#3 별도 Discovery.
- 전략 taxonomy(declarative intent) — trex-core#1 별도 Discovery.
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Source Discovery dcy-005 단일. 핵심 확정 결정: Option D(ledger write 응답 `*_after`, dcy-005 OQ-2) + degraded 신호(portfolio-hub 불가 → degraded=true + reason code, OQ-5) + view 3모드(all/buyable/held, OQ-11/12) + strategy_key(사람-읽기 안정 slug, OQ-9) + pause(portfolio_id 단위·Postgres 영속·즉시 복귀, OQ-10) + position 정합(수량 정확 일치·symbol 집계·KIS 잔고조회, OQ-8).
- **의존**: REQ-030 옵션 D 구현은 portfolio-hub#8 배포 대기. baseline 스케줄러(universe fetch)는 선구현 가능.
- **검증 순서 제약**: REQ-029 AC-5(warning·pause 마스킹) 검증은 REQ-031(warning 상태)과 REQ-032(pause 상태) 구현 완료 후 가능.
- **리스크 D-2**: KIS 당일 전량매도 후 잔고가 `hldg_qty=0` 으로 D-2 까지 남을 수 있음 → 0수량 잔고 처리 규칙 설계 단계 확정.
@@ -0,0 +1,71 @@
# BAT-008 Planning
- Status: draft
- Batch: BAT-008
- Profile: standard
## Included REQ
- REQ-027 (Integration): portfolio-hub read 포트 + 어댑터 + stub
- REQ-028 (Interface): 종목 스냅샷 read 메시지 계약 + envelope + degraded 신호
- REQ-029 (Interface): 자산 선별(slot overlay) + view 모드 + strategy_key 필터
- REQ-030 (Integration): 일일 스냅샷 스케줄러 + 옵션 D 현금/포지션 갱신
- REQ-031 (Exception): 포지션 정합 검증 + warning 마스킹
- REQ-032 (Interface): 런타임 pause/resume 제어
- REQ-033 (Documentation): 스냅샷 read 레이어 baseline 문서 정합
## Delivery Plan
- 의존 그래프 순서대로 구현한다.
1. **REQ-027** — `PortfolioHubReadPort` 포트(`trex_core/application/`) + portfolio-hub read 어댑터(`trex_core/adapters/portfolio/hub_read.py`) + 결정론 stub(`trex_core/adapters/portfolio/stub_read.py`). `runtime/config.py` 에 `daemon_id` 토큰 도출·캐시 로직 추가. 도메인 import 스윕 가드 확장.
2. **REQ-030 (baseline)** — 신규 스케줄러 스레드를 sync+thread daemon 에 추가(`_snapshot_scheduler` 스레드). `TREX_SNAPSHOT_SCHEDULE_KST`(기본 `07:30`) env 파라미터. universe fetch → 스냅샷 store 갱신. **옵션 D(`cash_balance_after`/`asset_quantity_after`) 갱신은 portfolio-hub#8 배포 후 별도 구현.**
3. **REQ-031** — BrokerPort 에 `get_balance() → list[BalanceItem(pdno, hldg_qty)]` 잔고조회 확장(KIS `TTTC8434R`/`VTTC8434R`, paper stub 결정론). 정합 검증 서비스: symbol 집계 vs broker per-symbol `hldg_qty` 수량 정확 일치, 불일치 자산 warning flag, 다음 정합 성공 시 자동 해제. 0수량 잔고 처리 규칙은 설계 단계 확정.
4. **REQ-032** — Postgres `snapshot_pause` 테이블(portfolio_id PK). CLI `trex-core pause <portfolio_id>` / `resume`. BAT-007 RPC 채널에 pause/resume 제어 메시지 핸들러 추가. daemon 재시작/일일 refresh 후에도 pause 영속.
5. **REQ-029** — 스냅샷 선별 서비스: `base(view)` 집합 산출 후 `mask(pending · warning · pause)` OR 합성 마스킹 후 `strategy_key` 직교 필터. SlotRepository(BAT-002) 재사용.
6. **REQ-028** — BAT-007 의 snapshot RPC consumer(`trex.snapshot.req`) 핸들러에 envelope 산출 로직 연결. degraded 분기: portfolio-hub 불가 탐지 → `degraded=true` + reason code + sanitized message. null 금지(`assets: []`).
7. **REQ-033** — REQ-027~032 구현 안정화 후 `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 정합.
8. **REQ-030 (옵션 D)** — portfolio-hub#8 배포 확인 후, ledger write 응답 소비 경로에 `cash_balance_after`/`asset_quantity_after` → 스냅샷 store 갱신 로직 추가. idempotent 갱신(duplicate write 처리).
## Dependencies
- Internal:
- REQ-027(read port) 선행 → REQ-028, REQ-029, REQ-030, REQ-031 모두.
- REQ-031(warning 상태), REQ-032(pause 상태) 선행 → REQ-029(마스킹 합성) 전체 검증 가능.
- REQ-029(선별 로직) 선행 → REQ-028(RPC handler 응답 산출).
- REQ-033 후행 → REQ-027~032 모두.
- BAT-002 `SlotRepository`(slot 상태) — REQ-029 slot overlay 재사용.
- BAT-004 `BrokerPort`(REQ-011/012) — REQ-031 잔고조회 확장.
- BAT-006 `LedgerPort`(REQ-017~019) — REQ-030 옵션 D ledger write hook.
- BAT-007 RabbitMQ snapshot RPC consumer(REQ-021/022) — REQ-028 핸들러 연결점.
- External:
- portfolio-hub `GET /v1/portfolios?managed_by_daemon_id=<id>` (`list_portfolios_v1`) — A-1 전제.
- **portfolio-hub#8** (`POST /v1/portfolios/{id}/ledger/trades` 응답에 `cash_balance_after`/`asset_quantity_after`) — REQ-030 옵션 D 의존. 배포 전까지 구현 대기.
- KIS 주식잔고조회(`TTTC8434R`/`VTTC8434R`, `GET /uapi/domestic-stock/v1/trading/inquire-balance`) — REQ-031. KIS 미가용 시 paper stub 결정론 fallback.
- RabbitMQ broker — BAT-007 transport 재사용(추가 provision 불필요).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 `adapters/portfolio` 계층 도입(read port 추상화 + 어댑터 + stub 구조 결정), 스냅샷 store 설계(in-memory snapshot + Postgres pause overlay 분리), warning state 머신(자동 임시 vs pause 영속 구분), KIS D-2 0수량 잔고 처리 규칙, masking 합성 순서(pending·warning·pause OR 적용 방식), degraded 신호 분기 결정(portfolio_hub_unavailable / snapshot_fetch_failed / cash_position_stale), daemon_id 토큰 도출·캐시 방식, 스케줄러 스레드 통합 방식을 구현 전에 design.md 에 고정해야 한다.
## Milestones
1. M1 — REQ-027: `PortfolioHubReadPort` 포트 + read 어댑터 + stub + daemon_id 토큰 도출 + import 스윕.
2. M2 — REQ-030 (baseline): 스케줄러 스레드 + universe fetch + 스냅샷 store 갱신(옵션 D 제외).
3. M3 — REQ-031: `BrokerPort.get_balance()` 확장(KIS/paper) + 정합 검증 + warning 상태 자동 해제.
4. M4 — REQ-032: `snapshot_pause` Postgres 테이블 + CLI + RPC 제어 핸들러 + 영속 검증.
5. M5 — REQ-029: 스냅샷 선별 서비스(base집합 + masking OR합성 + strategy_key 필터).
6. M6 — REQ-028: snapshot RPC 핸들러 + envelope 산출 + degraded 분기.
7. M7 — 통합 verification: end-to-end snapshot RPC 흐름, masking 합성(pending·warning·pause), strategy_key 필터, degraded 주입, pause 영속, 도메인 import 스윕.
8. M8 — REQ-033: baseline 3문서 정합.
9. M9(후속) — REQ-030 옵션 D: portfolio-hub#8 배포 확인 후 ledger write 응답 `*_after` 소비 + idempotent 갱신.
## Risks
- R-1 (Medium): **portfolio-hub#8 배포 일정 불확실** — REQ-030 옵션 D 구현 대기. 대응: baseline 스케줄러(fetch만)를 먼저 deliver 하고 옵션 D 는 M9 로 분리. 배포 확인 전 스냅샷 현금/포지션은 일일 fetch baseline 으로만 유지.
- R-2 (Medium): **KIS D-2 0수량 잔고** — 당일 전량매도 후 잔고가 `hldg_qty=0` 으로 D-2 까지 남음(KIS 015 문서). 0수량을 보유로 볼지 미보유로 볼지 처리 규칙 미확정. 대응: design 단계에서 처리 규칙 확정(예: `hldg_qty=0` 인 항목은 잔고 없음으로 취급).
- R-3 (Medium): **masking 합성 경계 케이스** — 같은 자산에 warning·pause 동시 적용, pending 상태에서 resume 호출, holding 슬롯과 portfolio-hub 보유 수량 불일치(holding→ledger flush 갭). 대응: AC 별 테스트 시퀀스를 verification.md 에 명시.
- R-4 (Low): **portfolio-hub read API 인터페이스** — `list_portfolios_v1` 의 `managed_by_daemon_id` 스코프 지원 여부, `PortfolioItem` 응답 스키마 확정 필요(A-1 전제). 대응: stub 어댑터로 선검증, 실 통합은 게이트 분리.
- R-5 (Low): **스케줄러 스레드 충돌** — 스케줄러 fetch 중 snapshot RPC 요청이 도착하면 partial 상태 읽기 위험. 대응: design 단계에서 snapshot store 락 또는 immutable swap 전략 확정.
@@ -0,0 +1,111 @@
# BAT-008 Verification
- Status: approved
- Batch: BAT-008
## Acceptance Mapping
- REQ-027:
- AC-1: stub + HTTP MockTransport 통합테스트에서 `PortfolioItem` 항목당 `symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params` 필드 + `price` 부재 확인.
- AC-2: 결정론 stub 가 네트워크 없이 고정 universe 반환 확인.
- AC-3: 도메인 import 스윕(`tests/domain/test_domain_import_sweep.py`)이 `pika`/`httpx`/`adapters.*` 0 import 회귀 가드.
- AC-4: HTTP MockTransport 가 `/v1/auth/whoami` 호출 + Bearer 토큰 헤더 + `daemon_id` 도출 확인. `TREX_DAEMON_ID` env 1차 출처 없음(코드 review).
- REQ-028:
- AC-1: envelope 응답이 정확히 `{assets, view, available_slots, degraded, reason, message}` 키 집합 + `assets` 가 항상 list(null 금지) 확인.
- AC-2: `SnapshotStore.set_degraded(...)` 후 envelope 가 `degraded=true reason=portfolio_hub_unavailable`. 실 운영 검증 — portfolio-hub 부재 환경에서 daemon 기동 후 snapshot RPC 가 같은 응답 반환.
- AC-3: 응답 항목에 `symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params` 만 존재, `price` 키 없음.
- REQ-029:
- AC-1: 같은 symbol holding + 다른 portfolio_id pending → `view=all` 에서 holding 자산만 포함, pending portfolio_id 마스킹.
- AC-2: `available_slots=0` + `view=buyable` → `assets=[]`.
- AC-3: `view=held` → holding 슬롯이 가리키는 portfolio_id 만 응답.
- AC-4: `strategy_key="target-value-v1"` → 일치 자산만 / 생략 시 마스킹 후 전체.
- AC-5: warning(`set_warning`) 또는 pause(`pause_store.pause`) 자산은 임의 view 에서 균일 마스킹.
- REQ-030:
- AC-1: `SnapshotScheduler.run_once()` 가 universe fetch + `SnapshotStore.swap` + reconcile 1회 수행 확인. 실 daemon boot 직후 동일 동작.
- AC-2 (옵션 D): **deferred — portfolio-hub#8 배포 후 evidence 수집**. `SnapshotStore.update_cash_position(pid, cash, qty)` wiring point 는 노출되어 있어 배포 후 LedgerWriter 의 ledger write 응답 소비 경로에서 호출 1줄 추가로 활성. 본 batch 에서는 wiring 단위테스트만 통과.
- AC-3 (옵션 D, duplicate idempotent): **deferred — portfolio-hub#8 배포 후 evidence 수집**.
- REQ-031:
- AC-1: hub symbol 집계 수량 ≠ broker `hldg_qty` → `SnapshotStore.set_warning(pid, True)` + envelope 에서 마스킹 확인.
- AC-2: 2 라운드 reconcile 시퀀스(불일치→일치)에서 warning 자동 해제 + 응답 복귀 확인.
- AC-3: paper broker `get_balance()` 가 fill aggregate 기반 결정론 잔고 반환 확인. 0수량 row drop 가드 포함.
- REQ-032:
- AC-1: `pause(pid)` 후 임의 view/strategy_key 요청에서 해당 portfolio_id 응답 마스킹.
- AC-2: PostgreSQL pause store 재인스턴스화(restart 시뮬레이션) 후에도 paused row 유지.
- AC-3: `resume(pid)` 후 다음 요청 envelope 에 복귀.
- AC-4: 동일 `PostgreSQLPauseStore` 를 CLI 와 RPC handler 가 공유 → CLI pause → RPC `action=list` 에 노출. 실 운영 검증 — daemon RPC pause → 같은 store 의 paused 목록 확인.
- REQ-033:
- AC-1: `docs/runtime-flows.md`/`docs/interface-contract.md`/`docs/project-structure.md` 모두에 BAT-008 섹션 + Change Log 항목 추가.
- AC-2: 베이스라인 ↔ 코드 정합 manual review — 본 verification 의 Manual 섹션 참조.
## Evidence
### Test
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" TREX_POSTGRESQL_TEST_DSN=$(grep TREX_POSTGRESQL_DSN .env | cut -d= -f2-) .venv/bin/python -m pytest -q`
- 결과: `185 passed, 1 skipped` (KIS sandbox token rate-limit 1건만 skip; 그 외 PG/RabbitMQ 게이트 모두 통과).
- REQ-027 AC-1: `tests/adapters/portfolio/test_hub_read.py::test_get_portfolios_scoped_by_managed_by_daemon_id` + `test_stub_read.py::test_stub_returns_fixed_universe_without_network`.
- REQ-027 AC-2: `tests/adapters/portfolio/test_stub_read.py::test_stub_returns_fixed_universe_without_network` + `test_stub_items_are_mutable_for_tests`.
- REQ-027 AC-3: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` (BAT-007 부터 유지, `httpx`/`pika`/`adapters.*` 금지 prefix 회귀 가드).
- REQ-027 AC-4: `tests/adapters/portfolio/test_hub_read.py::test_whoami_returns_token_daemon_id` (URL/Bearer 헤더/응답 디코드 + daemon_id 추출).
- REQ-028 AC-1: `tests/application/test_snapshot_handler.py::test_envelope_shape_returned` + `tests/application/test_snapshot_selection.py::test_empty_envelope_assets_is_list_not_null`.
- REQ-028 AC-2: `tests/application/test_snapshot_handler.py::test_degraded_signal_does_not_leak_credentials` + `tests/application/test_snapshot_selection.py::test_degraded_signal_in_envelope`. 실 daemon 운영 검증에서 portfolio-hub 부재 환경에서 같은 응답 발생 확인.
- REQ-028 AC-3: `tests/application/test_snapshot_selection.py::test_envelope_shape_no_price_field`.
- REQ-029 AC-1: `tests/application/test_snapshot_selection.py::test_view_all_includes_holding_excludes_pending_for_same_symbol`.
- REQ-029 AC-2: `tests/application/test_snapshot_selection.py::test_view_buyable_with_zero_available_slots_returns_empty`.
- REQ-029 AC-3: `tests/application/test_snapshot_selection.py::test_view_held_returns_only_holding_slot_assets`.
- REQ-029 AC-4: `tests/application/test_snapshot_selection.py::{test_strategy_key_orthogonal_filter,test_strategy_key_missing_includes_everything_unmasked}`.
- REQ-029 AC-5: `tests/application/test_snapshot_selection.py::{test_warning_overlay_masks_asset_uniformly_across_views,test_pause_overlay_masks_asset}`.
- REQ-030 AC-1: `tests/application/test_snapshot_scheduler.py::test_run_once_fetches_universe_and_swaps_store` + `::test_swap_preserves_warning_for_carried_over_portfolio`. 실 daemon boot 후 첫 fetch 동작 확인.
- REQ-030 AC-2/AC-3: **deferred — portfolio-hub#8 배포 대기**. wiring point 테스트(`test_snapshot_scheduler.py::test_option_d_update_cash_position_overrides_baseline`) 가 `SnapshotStore.update_cash_position` 의 동작 보장.
- REQ-031 AC-1: `tests/application/test_reconciliation.py::test_mismatch_raises_warning_and_masks_asset`.
- REQ-031 AC-2: `tests/application/test_reconciliation.py::test_subsequent_match_auto_clears_warning`.
- REQ-031 AC-3: `tests/application/test_reconciliation.py::test_paper_broker_returns_deterministic_balance` (paper fill aggregate + 0수량 drop 시나리오).
- REQ-032 AC-1: `tests/application/test_pause_handler.py::test_pause_then_list` + `tests/application/test_snapshot_selection.py::test_pause_overlay_masks_asset`.
- REQ-032 AC-2: `tests/adapters/portfolio/test_postgresql_pause_store.py::test_durability_simulated_via_fresh_store_instance` (PG 게이트).
- REQ-032 AC-3: `tests/application/test_pause_handler.py::test_resume_clears_pause`.
- REQ-032 AC-4: `tests/application/test_pause_handler.py::test_cli_pause_visible_to_rpc_list_via_shared_store`. 실 daemon 운영 검증 — RPC pause → 같은 store list 노출 확인.
### Manual
- baseline ↔ 코드 정합 리뷰 (REQ-033 AC-1/AC-2):
- `docs/runtime-flows.md` 의 신규 5 섹션(`일일 스냅샷 refresh`, `포지션 정합 검증`, `pause/resume 제어`, `스냅샷 RPC 응답 산출`, `broker degrade 분기` 인접 위치) 가 실제 코드와 정합:
- `SnapshotScheduler.run_once()` 의 boot+tick 시퀀스 → `trex_core/application/snapshot_scheduler.py:run_forever`
- `reconcile_positions` symbol 집계 + warning set/clear → `trex_core/application/reconciliation.py`
- pause/resume CLI ↔ RPC ↔ `snapshot_pause` table → `trex_core/runtime/cli.py` + `trex_core/adapters/portfolio/pause_store.py` + `trex_core/application/messaging_handlers.py::make_pause_rpc_handler`
- envelope builder 의 base/mask/strategy_key 합성 → `trex_core/application/portfolio.py::build_snapshot_envelope`
- `docs/interface-contract.md` 의 `trex.snapshot.req` envelope 스키마 + `trex.pause.req` control RPC + `PortfolioHubReadPort` Protocol + `SnapshotStore`/`PauseStore`/`SnapshotScheduler` + `BrokerPort.get_balance()` 확장 섹션이 실제 시그니처와 정합.
- `docs/project-structure.md` 디렉토리 트리에서 `trex_core/adapters/portfolio/`, `trex_core/application/{portfolio,reconciliation,snapshot_scheduler}.py`, `trex_core/runtime/cli.py`, `003_create_snapshot_pause.sql`, daemon 의 6-스레드 구성, `trex-core` CLI console_script 모두 실제 트리(`ls -R trex_core/`)와 일치.
- code-search:
- `grep -r "from fastapi\|from uvicorn" trex_core/` → 0 (BAT-007 회귀 유지).
- `grep -r "import pika\|import httpx" trex_core/domain/` → 0 (NFR-1).
- `find trex_core/adapters/portfolio -type f -name '*.py'` → `__init__/hub_read/stub_read/pause_store.py` 4 파일 존재.
- `grep "PAUSE_REQ_QUEUE" trex_core/adapters/messaging/` → topology 상수 + rabbitmq declare 두 곳 노출.
### Logs
- `.venv/bin/ruff check trex_core tests` → `All checks passed!`.
- 실 RabbitMQ + PostgreSQL evidence: `127.0.0.1:5672` admin/admin + 로컬 PG 로 daemon 기동 후 snapshot RPC (view=all/buyable/held + strategy_key), pause/resume RPC, 응답 envelope 키/필드 검증 모두 통과. portfolio-hub 부재 환경에서 `degraded=true reason=portfolio_hub_unavailable` 정상 노출.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음.
## Human Approval Memo
- 승인자: Justin Ko (운영자 위임 — dcy-005 confirmed-by + REQ-030 Approved follow-up "portfolio-hub#8 배포 추적 후 옵션 D 구현 착수" 정책 적용)
- 승인 시각: 2026-06-06 (KST)
- 수용 범위 / 스킵 항목:
- **REQ-030 AC-2 / AC-3 (옵션 D — ledger write `*_after` 갱신)** : portfolio-hub#8 (`POST /v1/portfolios/{id}/ledger/trades` 응답 본문에 `cash_balance_after`/`asset_quantity_after`) 배포 전제. portfolio-hub 측 별도 배포(REL-003 등) 완료 전까지 trex-core 구현은 대기 — dcy-005 A-7 명시. 본 batch 의 wiring point(`SnapshotStore.update_cash_position`)는 이미 노출되어 있어 배포 후 LedgerWriter 의 ledger write 응답 소비 경로에서 호출 1줄 추가로 활성화 가능.
- 잔여 리스크:
- 옵션 D 활성 후 first ledger write 응답에서 실제 `*_after` 값을 사용해 idempotent 갱신이 동작하는지 통합 검증 필요(별도 후속 작업).
- KIS D-2 0수량 잔고 처리 규칙(설계 Decision 7)은 paper broker 회귀 가드만 있고 실 KIS 환경 관찰 evidence 부재 — 운영 단계에서 한 번 관찰 후 운영 노트화 권장.
- 스케줄러 daily fire 동작(boot 즉시 fire 외) 은 실 데몬 운영 후 첫 07:30 KST 도래 시 관찰 권장.
- 승인 근거:
- dcy-005 confirm-discovery 단계 `Handoff Decision: 승인` + REQ-030 confirm-req 단계 `Follow-up Action: portfolio-hub#8 배포 추적 후 옵션 D 구현 착수` 비차단 정책.
- 본 batch 의 M1-M8 / REQ-027~029, REQ-031~033 8개 acceptance criteria 모두 evidence 연결 완료.
- REQ-030 AC-1 (baseline 스케줄러)는 evidence 연결 완료. AC-2/AC-3 은 외부 의존(portfolio-hub#8) 대기 항목이며 본 batch wiring point 가 이미 준비됨.
- 후속 트래킹:
- portfolio-hub#8 배포 완료 알림 수신 시 별도 minor-change batch 로 옵션 D 구현 + REQ-030 재verify → REQ-030 status Approved → Implemented 전환.
+1
View File
@@ -15,3 +15,4 @@
| BAT-005 | batch-lite | release-candidate | REQ-016 | dcy-003 | [bat-005](./bat-005_20260604_kis-token-redis-cache/) |
| BAT-006 | standard | release-candidate | REQ-017, REQ-018, REQ-019, REQ-020 | dcy-004 | [bat-006](./bat-006_20260604_portfolio-hub-ledger-integration/) |
| BAT-007 | standard | release-candidate | REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026 | dcy-006 | [bat-007](./bat-007_20260605_strategy-module-rabbitmq-transport/) |
| BAT-008 | standard | release-candidate | REQ-027, REQ-028, REQ-029, REQ-030, REQ-031, REQ-032, REQ-033 | dcy-005 | [bat-008](./bat-008_20260606_portfolio-hub-snapshot-read-layer/) |