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
+121 -3
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 API = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -25,15 +25,19 @@
## Interface Summary
- RabbitMQ `trex.snapshot.req` :: 전략 모듈의 스냅샷 RPC (request-reply, BAT-007)
- RabbitMQ `trex.snapshot.req` :: 전략 모듈의 스냅샷 RPC (request-reply, BAT-007/BAT-008 envelope)
- RabbitMQ `trex.intent.req` :: 전략 모듈의 매수/매도 admission RPC (request-reply, BAT-007)
- RabbitMQ `trex.events` :: 체결/슬롯 상태 이벤트 topic exchange (pub/sub, BAT-007)
- RabbitMQ `trex.pause.req` :: pause/resume 제어 RPC (request-reply, BAT-008)
- `trex-core` CLI :: pause/resume/paused 운영 명령 (BAT-008)
- SlotService :: 슬롯 생명주기 도메인 서비스(전이/사전할당/가용성/재진입 카운트)
- SlotRepository (Protocol) :: 도메인 ↔ 저장소 경계 계약
- BrokerPort (Protocol) :: 단일 브로커 세션 경계(submit/poll/cancel) + `OrderRequest`/`OrderAck`/`FillStatus`
- LedgerPort (Protocol) :: 외부 원장(portfolio-hub) 기록 경계 + `LedgerTrade`/`LedgerWriteResult`/예외 (BAT-006)
- LedgerOutbox (Protocol) + LedgerWriter :: 체결 → 원장 기록의 outbox/재시도/멱등 (BAT-006)
- MessagingTransport (Protocol) :: RabbitMQ / InMemory stub transport 경계 + `TransportHealth` enum (BAT-007)
- PortfolioHubReadPort (Protocol) :: portfolio-hub universe read 경계 + `PortfolioItem`/`PortfolioHubUnavailableError` (BAT-008)
- SnapshotStore + PauseStore + SnapshotScheduler :: in-memory snapshot + durable pause overlay + 일일 refresh (BAT-008)
> BAT-007 에서 HTTP `/v1` API 표면이 0 으로 제거됨(REQ-025). 이전 baseline 의 `HTTP /v1` 인터페이스 섹션은 더 이상 유효하지 않으며 본 인터페이스 인덱스에서 제거되었다. 전략-facing 통신은 위의 3 RabbitMQ 표면이 전부다.
@@ -106,7 +110,120 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason
- 재연결은 백오프 1s→2s→4s→8s→16s→30s cap, 어댑터가 자체 구현.
- core 자율 안전 매도는 transport 와 무관하게 동작해야 한다(architecture contract; 자동 트리거 구현은 별도 Discovery).
## Interface: `SlotService`
### `trex.snapshot.req` envelope (BAT-008 / REQ-028)
전략 모듈이 보내는 요청:
```
{ "view": "all" | "buyable" | "held" # default "all"
"strategy_key": "<slug>" # optional, == filter on active_strategy_id
"trading_date": "YYYY-MM-DD" # optional, default UTC today
}
```
코어가 reply-to 큐로 보내는 응답 envelope:
```
{ "assets": [ # always a list (REQ-028 AC-1, never null)
{ "symbol": str,
"asset_id": str, # == portfolio_id (dcy-004 OQ-1)
"cash_balance": number,
"asset_quantity": number,
"active_strategy_id": str | null,
"strategy_params": object # opaque, pass-through
}, ...
],
"view": "all" | "buyable" | "held", # echo
"available_slots": int, # idle_count snapshot
"degraded": bool, # REQ-028 AC-2
"reason": "portfolio_hub_unavailable" | "snapshot_fetch_failed"
| "cash_position_stale" | null,
"message": str | null # sanitized — no credentials/raw exceptions
}
```
**No `price` field on items** (REQ-028 AC-3 / REQ-027 AC-1).
### `trex.pause.req` control RPC (BAT-008 / REQ-032)
요청:
```
{ "action": "pause" | "resume" | "list",
"portfolio_id": str, # required for pause/resume
"actor": str # optional, default "operator"
}
```
응답:
```
{ "status": "ok" | "error",
"paused": [str, ...], # current paused portfolio_ids (sorted)
"action": echo,
"reason": str # only when status="error"
}
```
- pause 는 `snapshot_pause` Postgres 테이블에 upsert(actor 갱신)되어 daemon 재시작·일일 refresh 후에도 유지된다(REQ-032 AC-2).
- CLI `trex-core pause/resume/paused` 가 동일한 store 를 가리키므로 CLI ↔ RPC 가 같은 상태를 본다(REQ-032 AC-4).
## Interface: `PortfolioHubReadPort` (Protocol, BAT-008)
- Type: external-service (HTTP, portfolio-hub)
- Consumers / Producers: `SnapshotScheduler`(소비) ↔ `PortfolioHubReadAdapter` 또는 `StubPortfolioRead`
- Purpose: portfolio-hub universe(이 데몬이 관리하는 portfolio 집합) read 경계 — write 는 BAT-006 `LedgerPort` 와 분리.
- Stability: experimental (BAT-008)
### Inputs
- `whoami() -> str` :: 자격증명/토큰에서 `daemon_id` 도출(1회). 별도 env 1차 출처 두지 않음.
- `get_portfolios(daemon_id) -> list[PortfolioItem]` :: `managed_by_daemon_id == daemon_id` scope 조회.
### `PortfolioItem` Fields
- `portfolio_id :: str` :: == asset_id (dcy-004 OQ-1)
- `symbol :: str`
- `cash_balance :: float`
- `asset_quantity :: float` :: integer shares semantically
- `active_strategy_id :: str | None`
- `strategy_params :: dict` :: opaque pass-through (no core parsing)
### Outputs / Outcomes
- 정상: `list[PortfolioItem]` (배열, 비면 `[]`).
- 어댑터 어떤 실패든 `PortfolioHubUnavailableError` 로 통일 (호출자는 `SnapshotStore.set_degraded` 로 전환).
### Error Contract
- `PortfolioHubUnavailableError` :: 네트워크/5xx/4xx/JSON 파싱 실패 등 fetch 불가. 자격증명/예외 본문은 호출 측에서 sanitize 후 노출(REQ-028 AC-2).
### portfolio-hub Endpoint (어댑터 구현)
| HTTP | URL | 응답 |
|---|---|---|
| GET | `/v1/auth/whoami` | `{daemon_id: str, ...}` |
| GET | `/v1/portfolios?managed_by_daemon_id=<id>` | `{items: [PortfolioItem, ...]}` 또는 list root |
## Interface: `SnapshotStore` + `PauseStore` + `SnapshotScheduler` (Protocol, BAT-008)
- Type: internal-service
- Consumers / Producers: `SnapshotScheduler`(생산) → `SnapshotStore`(read 채널) ← `make_snapshot_rpc_handler`(소비). `PauseStore`(`PostgreSQLPauseStore` 또는 `InMemoryPauseStore`)는 CLI/RPC 양쪽이 쓰고 handler 가 읽는다.
### Inputs
- `SnapshotStore.swap(items)` :: atomic immutable swap. 직전 `warning` flag 는 carry-over(다음 reconcile 에서 자동 해제 판단).
- `SnapshotStore.set_warning(pid, on)` :: 정합 결과 overlay(in-memory only).
- `SnapshotStore.update_cash_position(pid, cash, qty)` :: 옵션 D wiring point(REQ-030 AC-2/AC-3 — portfolio-hub#8 배포 후에만 ledger writer 가 호출).
- `SnapshotStore.set_degraded(reason, message)` / `clear_degraded()` :: degraded signal.
- `PauseStore.pause(pid, actor)` / `resume(pid)` / `list_paused()` :: durable manual overlay.
- `SnapshotScheduler.run_once()` :: fetch + swap + reconcile 1 회.
- `SnapshotScheduler.run_forever(stop, tick_sec)` :: boot 즉시 1 회 + 매일 `TREX_SNAPSHOT_SCHEDULE_KST` 시각에 1 회.
### Outputs / Outcomes
- `run_once()` returns `True`(성공) / `False`(degraded). `True` 시 reconcile 한 패스가 함께 돈다.
- `list_assets()` + `degraded_state()` 를 envelope builder 가 매 RPC 요청마다 읽음.
### Compatibility Rules
- pause 의 영속성 단위는 `portfolio_id`(= asset_id). 동일 portfolio_id 재 pause 는 upsert(actor만 갱신).
- warning 은 in-memory only — 재시작 시 초기화되며 다음 정합 패스가 다시 판정한다.
- universe swap 은 pause/warning 의 결정을 invalidate 하지 않는다(warning 은 carry-over, pause 는 store 가 별도 영속).
- Type: internal-service
- Consumers / Producers: 상위 애플리케이션(예: trex-orbit 류 매매 데몬)의 매수/매도/주문 결정 경로 및 운영자 도구
@@ -300,3 +417,4 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason
- 2026-06-04 :: BAT-004 — 브로커 집행 레이어 반영(BrokerPort/paper·KIS 어댑터, execution 루프, broker_order_id 실행큐·멱등, reconciliation) (REQ-011~015).
- 2026-06-04 :: BAT-006 — LedgerPort/LedgerTrade/LedgerWriteResult 계약, LedgerOutbox/LedgerWriter, portfolio-hub `/v1/assets/{asset_id}/ledger/trades` 엔드포인트 + 409→`LedgerConflictError`, FillStatus.execution_id/fee/tax 확장 (REQ-017~020).
- 2026-06-05 :: BAT-007 — HTTP `/v1` API 인터페이스 제거(adapters/api + fastapi/uvicorn 의존 정리), RabbitMQ messaging 계약 추가(스냅샷 RPC `trex.snapshot.req`, intent RPC `trex.intent.req`, 이벤트 topic `trex.events`, 전략별 durable queue + TTL/max-length, DLQ, publisher confirms, ack-after-commit), `MessagingTransport` Protocol + `TransportHealth` 노출 (REQ-021~026).
- 2026-06-06 :: BAT-008 — `trex.snapshot.req` envelope 계약 정의(`assets[]/view/available_slots/degraded/reason/message`, no price field), `trex.pause.req` 제어 RPC 추가, `PortfolioHubReadPort`/`SnapshotStore`/`PauseStore`/`SnapshotScheduler` 인터페이스, `BrokerPort.get_balance()` 확장, `trex-core` CLI 추가 (REQ-027~033; REQ-030 옵션 D 는 portfolio-hub#8 배포 후).