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
+42 -1
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-05 (BAT-007)
- Last Updated (KST): 2026-06-06 (BAT-008)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -67,6 +67,46 @@
3. 전략별 durable queue(`trex.events.strategy.{strategy_id}`)는 `*.{strategy_id}.#` 패턴으로 바인딩 → broker 단 필터로 자기 자산 이벤트만 수신.
4. 전략 down 중 적체된 메시지는 catch-up 으로 복구; TTL(1h) / max-length(10k) 초과는 `drop-head` + 후속 스냅샷 RPC reconcile.
## Flow: `일일 스냅샷 refresh` (BAT-008 / REQ-030)
데몬 lifespan 의 `trex-snapshot-scheduler` 스레드가 portfolio-hub universe 를 fetch 해 in-memory 스냅샷을 atomic-swap 으로 교체하고, 같은 패스에서 정합 검증을 수행한다.
1. boot: `SnapshotScheduler.run_once()` 한 번 호출 → 첫 universe 로딩(또는 degraded 마킹).
2. tick(`TREX_SNAPSHOT_TICK_SEC`, 기본 60s): 현재 KST 시각이 `TREX_SNAPSHOT_SCHEDULE_KST`(기본 07:30) 이상이고 오늘 아직 안 돌았으면 fire.
3. fire 단계: `PortfolioHubReadPort.whoami()` 로 daemon_id 도출(1회 캐시) → `get_portfolios(daemon_id)` 로 universe 페치 → `SnapshotStore.swap(items)` atomic 교체 → `reconcile_positions(store, broker)` 정합 검증.
4. 실패 분기: `PortfolioHubUnavailableError` → `degraded = portfolio_hub_unavailable`; 응답 파싱 실패 → `snapshot_fetch_failed`. 둘 다 다음 RPC 응답 envelope 에 즉시 노출(REQ-028 AC-2).
## Flow: `포지션 정합 검증` (BAT-008 / REQ-031)
`reconcile_positions` 는 portfolio-hub 보유 수량을 symbol 별로 합산해 broker `get_balance()` 결과와 비교한다.
1. `BrokerPort.get_balance() → list[BalanceItem(symbol, hldg_qty)]` 조회. KIS 어댑터는 `TTTC8434R`/`VTTC8434R` `inquire-balance`, paper 는 결정론 fill aggregate. **0수량 row 는 양쪽 모두 drop**(D-2 leftover 처리, Decision 7).
2. 자산을 `symbol` 로 그룹화해 `hub_total = sum(asset_quantity)` 계산 → broker 의 같은 symbol `hldg_qty` 와 비교.
3. 불일치: 해당 symbol 의 모든 portfolio_id 에 `SnapshotStore.set_warning(pid, True)` → 다음 envelope 부터 응답에서 마스킹. 첫 raise 시 한 번 로그.
4. 일치: 이전에 `warning` 이었던 자산은 자동 해제 → 다음 envelope 부터 다시 포함. resume 과 달리 자동·임시 overlay(영속 X).
## Flow: `pause/resume 제어` (BAT-008 / REQ-032)
운영자가 자산을 매매에서 제외/복귀시키는 두 경로(CLI + RPC)는 동일한 `snapshot_pause` Postgres 테이블을 가리킨다.
1. CLI: `trex-core pause <portfolio_id>` / `trex-core resume <portfolio_id>` → `PostgreSQLPauseStore.pause/resume` 호출(`portfolio_id` PK upsert/delete).
2. RPC: 전략 모듈/운영자가 `trex.pause.req` 큐에 `{action: pause|resume|list, portfolio_id, actor}` 발행 → `make_pause_rpc_handler` 가 같은 store 호출 → `{status, paused[], action}` 응답.
3. 응답 산출: `build_snapshot_envelope` 가 `pause_store.list_paused()` 를 매 요청마다 읽음 → `mask(pending | warning | pause)` 합성. resume 후 **다음 요청부터 즉시 복귀**(별도 reload 불필요).
4. 영속: daemon 재시작 + 일일 refresh 후에도 `snapshot_pause` row 유지(universe swap 은 pause 를 만지지 않음).
## Flow: `스냅샷 RPC 응답 산출` (BAT-008 / REQ-028, REQ-029)
`trex.snapshot.req` 메시지 처리.
1. 요청 payload 디코드: `{view ∈ {all, buyable, held}, strategy_key?, trading_date?}`.
2. `slot_service.compute_slot_capacity(today, total_slots)` → `available_slots = idle_count`.
3. `build_snapshot_envelope(store, pause_store, slot_repo, view, strategy_key, available_slots, trading_date)`:
- base 집합: ALL = `universe ∪ holding`, BUYABLE = `universe − (pending ∪ holding)`(`available_slots=0` 이면 `[]`), HELD = `holding 슬롯만`.
- 마스킹: pending(slot) OR warning(REQ-031) OR pause(REQ-032) — 어느 하나라도면 제외. slot overlay > portfolio-hub 보유 여부(불변식 A).
- `strategy_key` 일치(`==`) 직교 필터.
- degraded 상태가 있으면 `degraded=true + reason + sanitized message` 동봉.
4. envelope: `{assets[], view, available_slots, degraded, reason, message}` — `assets` 는 항상 배열, null 금지. 항목 필드는 `symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params`, **price 필드는 없음**.
## Flow: `broker degrade 분기` (BAT-007 / REQ-024)
1. transport connection drop → 어댑터가 `_mark_disconnected()` + 백오프(1s→2s→4s→8s→16s→30s cap) 재연결.
@@ -157,3 +197,4 @@
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
- 2026-06-04 :: BAT-006 — ledger 통합 반영(체결→ledger outbox enqueue, LedgerWriter retry/dedup/409 흐름, FillStatus.execution_id/fee 운반, slot 전이와 ledger write 분리) (REQ-017~020).
- 2026-06-05 :: BAT-007 — transport 전환(HTTP `/v1` API 제거, RabbitMQ 메시징 도입: 스냅샷/intent RPC + 이벤트 topic, sync+thread daemon, intent_log 멱등 ack-after-commit, publisher confirms + DLQ, per-strategy durable queue + TTL/max-length, broker degrade 분기) (REQ-021~026).
- 2026-06-06 :: BAT-008 — portfolio-hub 스냅샷 read 레이어 추가(`PortfolioHubReadPort` + hub_read/stub_read 어댑터, `SnapshotStore` in-memory + `snapshot_pause` Postgres, `SnapshotScheduler` 일일 refresh, `reconcile_positions` 정합 검증 + `BrokerPort.get_balance()` 확장, `trex.snapshot.req` envelope 응답 + degraded 신호, `trex.pause.req` 제어 RPC, `trex-core` CLI) (REQ-027~033; 옵션 D AC-2/AC-3 은 portfolio-hub#8 배포 후 별도 구현).