feat(ledger): dcy-018 option D cash/position write-back + 추정 현금 (BAT-023)

ledger write 응답의 portfolio-hub 권위 cash/quantity 를 스냅샷에 write-back 해
매수/매도 직후 "수량 live·현금 stale" 비대칭 제거(option D 활성화). portfolio-hub#8
(*_after 응답) 동작 중 확인으로 그간 deferred 됐던 REQ-030 을 닫음.

거버넌스: 전부 change-req (신규 REQ 없음, dcy-018 OQ-3 a).
- REQ-017: record_trade → LedgerWriteOutcome{result, cash_balance_after,
  asset_quantity_after}. 어댑터가 응답 root flat *_after 파싱(new+duplicate, 누락 None)
- REQ-019: LedgerWriter 에 SnapshotStore 주입 → new+duplicate write-back
  (best-effort, *_after 절대값이라 멱등). LedgerOutbox.pending_cash_delta()
- REQ-029: 추정 현금 오버레이(_apply_cash_estimate, cash_estimated 라벨, OQ-4 i)
  + 슬롯 오버레이 유지(OQ-1 B). 미정산 outbox delta 기반, write-back 수렴
- REQ-030: option D 활성화 → Approved→Implemented

best-effort 격리(NFR-1): write-back/추정 실패가 ledger 멱등·청산 무차단.
배치 번호: 동시 커밋된 BAT-022(slot-state, dcy-016)와 충돌해 BAT-023 으로 배정.

검증: 단위 350 passed/35 skipped, 인프라 게이트(PG+RabbitMQ) 385 passed, ruff clean.
신규 test_ledger_option_d + adapter/PG 테스트. BAT-023 release-candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 21:50:56 +09:00
co-authored by Claude Opus 4.8
parent 63da87c9b3
commit 2bfecd59a8
26 changed files with 969 additions and 34 deletions
+6 -4
View File
@@ -4,7 +4,7 @@
- Baseline Seed: .stage-pilot/bootstrap/baseline.yaml
- Status: draft
- Owner: project maintainers
- Last Updated (KST): 2026-06-08 (BAT-021)
- Last Updated (KST): 2026-06-08 (BAT-023)
- Source Discovery / Batch: bootstrap-baseline (pre-Discovery); 데몬/HTTP 흐름 = dcy-002 / BAT-002
- Primary Runtime: daemon (HTTP API) + library (slot 도메인)
@@ -129,6 +129,7 @@
- `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 필드는 없음**.
5. 추정 현금 오버레이(dcy-018/REQ-029, OQ-4): 미정산(pending) ledger outbox 행이 있는 자산은 `cash_balance += pending cash delta`(매수 −, 매도 +) + `cash_estimated:true` 라벨로 노출 → option D write-back 도착 시 권위값으로 수렴(pending 제거). best-effort(실패 시 응답 불변). 수량은 슬롯 오버레이(REQ-029)가 정확값이라 추정 불요.
## Flow: `per-slot 워치독 갱신` (BAT-010/011 / REQ-034, REQ-040)
@@ -244,9 +245,10 @@
1. `LedgerOutbox.list_pending(limit)` 으로 pending work item 을 FIFO 로 가져온다. **백오프 중(`next_retry_at > now`)인 행은 건너뛴다**(dcy-015/REQ-019 head-of-line 보호, NFR-3).
2. row 의 현재 상태가 이미 `completed` 면 (e.g. 동시/재처리) `duplicate_completed` 로 마킹하고 어댑터를 호출하지 않는다.
3. `LedgerPort.record_trade(trade)` 호출.
- `LedgerWriteResult.NEW` → outcome `completed`, row status `completed`.
- `LedgerWriteResult.DUPLICATE_OK`(어댑터 server-side dedup) → outcome `duplicate_completed`, row status `completed`, ledger 중복 0.
3. `LedgerPort.record_trade(trade)` 호출 → `LedgerWriteOutcome{result, cash_balance_after?, asset_quantity_after?}`.
- `result=NEW` → outcome `completed`, row status `completed`.
- `result=DUPLICATE_OK`(어댑터 server-side dedup) → outcome `duplicate_completed`, row status `completed`, ledger 중복 0.
- **option D write-back(dcy-018/REQ-019/030)**: `new`·`duplicate` outcome 의 `cash_balance_after`/`asset_quantity_after`(None 아님)를 `SnapshotStore.update_cash_position(asset_id, …)` 로 반영(best-effort — 실패해도 ledger outcome 무차단, NFR-1). `*_after` 가 **절대 권위값**이라 duplicate 도 멱등. 응답 누락(구 hub)·409/422 는 write-back skip.
- `LedgerConflictError`(portfolio-hub 409) → outcome `conflict_blocked`, row status `conflict_blocked` (자동 재시도 중단, 운영자 해소 대상).
- `LedgerPermanentError`(portfolio-hub **404/400/422**, dcy-015/REQ-017, issue #13) → outcome `permanent_failure`, row status **`delivery_failed`**(터미널, 재시도 0). 재시도로 해소 불가한 클라이언트 오류(미등록 포트폴리오 등) → 즉시 중단 + WARN.
- `LedgerRetryableError`(timeout/5xx/network/408/429) → **지수 백오프 + jitter**(`delay = min(base × 2^(attempts-1), cap)`, `next_retry_at` 설정) 후 다음 due tick 에 재시도. **`max_attempts` 초과 시** outcome `dead_lettered`, row status **`delivery_failed`**(dead-letter, 터미널) + WARN. 무한 1초 재시도 제거.