Files

142 lines
9.2 KiB
Markdown

---
id: "dcy-0001"
status: confirmed
---
# 포트폴리오 목록 응답에 ledger 기반 투자금 집계 추가
## Problem
TREX 프로젝트 보드의 포트폴리오 테이블 개선 초안에서 `portfolio-hub` 소유 구현 범위를 분리한다. 소비자 `fourblocks2020/trex-web`이 포트폴리오 행마다 단건 returns summary를 N+1로 요청하거나 ledger event를 자체 재해석하지 않고도 투자금액과 수익 지표를 표시할 수 있도록, `GET /v1/portfolios` 목록 응답에 `portfolio-hub` 권위의 manual ledger 집계가 필요하다.
당시 `GET /v1/portfolios`는 `PortfolioListResponse{portfolios, total}`을 반환하고 각 `PortfolioItem`에 `cash_balance`, `asset_quantity`, `price`, `profit_buffer` 등을 포함했지만 투자 원금 집계는 제공하지 않았다. 반면 기존 `GET /v1/portfolios/{portfolio_id}/returns/summary`는 `total_invested`, `total_withdrawn`, `net_contributed_capital`, `cumulative_profit_withdrawn`을 제공했다. 목록과 단건 API의 의미가 분리되면 소비자가 별도 조회·재계산을 하게 되고, `PROFIT`과 `WITHDRAW` 또는 취소된 원거래를 잘못 해석할 위험이 있었다.
## Evidence
### 원문과 승인 근거
- GitHub Issue #23: https://github.com/fourblocks2020/portfolio-hub/issues/23
- 제목: `포트폴리오 목록 응답에 ledger 기반 투자금 집계 추가`
- 생성: 2026-08-13
- 상태: CLOSED
- 상위 보드 초안: https://github.com/orgs/fourblocks2020/projects/7
- 소비자 구현 이슈: https://github.com/fourblocks2020/trex-web/issues/13
- 요청자 `genichin`은 2026-08-13 OQ-23-1에서 A안인 **유효 원장 기준 집계**를 선택했다.
- 요청자는 2026-08-13 이 문서를 #23 원문에 기반한 승인 상태의 Discovery로 작성하도록 명시했다.
### 당시 계약
- `GET /v1/portfolios`는 `PortfolioListResponse{portfolios, total}`을 반환했다.
- `PortfolioItem`에는 투자 원금 집계가 없었다.
- 단건 `GET /v1/portfolios/{portfolio_id}/returns/summary`는 다음 네 값을 제공했다.
- `total_invested`
- `total_withdrawn`
- `net_contributed_capital`
- `cumulative_profit_withdrawn`
- PostgreSQL 산식은 manual ledger에서 `INVEST`, `WITHDRAW`, `PROFIT`을 분리했다.
- `net_contributed_capital = INVEST - WITHDRAW`
- `cumulative_profit_withdrawn = PROFIT`
- `PROFIT`은 투자금 회수인 `WITHDRAW`와 의미가 다르다.
### 확정 결정: OQ-23-1 — REVERSAL은 원거래 효과를 상쇄한다
- 결정: A안, 유효 원장 기준 집계
- 결정자: 요청자 `genichin`
- 결정일: 2026-08-13
- `REVERSAL`이 참조하는 `INVEST`, `WITHDRAW`, `PROFIT` 원거래는 순투자현금·누적 수익 인출액 집계에서 제외한다.
- `REVERSAL` 원장 자체를 금액 action으로 다시 합산하지 않는다.
- `net_contributed_capital`과 `cumulative_profit_withdrawn`은 역사적 action 단순 누계가 아니라 현재 유효한 manual ledger 기준 금액이다.
- 포트폴리오 목록과 기존 단건 `returns/summary`는 동일한 유효 원장 판정과 산식을 사용한다.
- 미취소 거래, `INVEST` 취소, `WITHDRAW` 취소, `PROFIT` 취소를 회귀 검증하며 기존 이중 reversal 거부 규칙을 유지한다.
- 향후 단순 역사 누계가 필요하면 별도 필드로 추가하고 기존 두 필드의 의미를 조용히 되돌리지 않는다.
- 이 결정으로 원문의 Open Question은 모두 해소됐다.
### 현재 구현 증거
- PR #24의 merge commit `35e85981af845489236c18d709d0545c830e7f03`이 `main`에 반영됐다.
- 구현 경로:
- `app/schemas/v1.py`
- `app/repositories/portfolio_repository.py`
- `app/repositories/postgres.py`
- `app/api/v1_portfolios.py`
- `docs/interface-contract.md`
- `docs/runtime-flows.md`
- 회귀 경로:
- `tests/test_portfolio_ledger_aggregates.py`
- `tests/test_v1_portfolios_list.py`
- `tests/test_v1_portfolios_manual_ledger.py`
- 당시 저장된 실행 증거는 focused **38 passed**, clean detached worktree 전체 **598 passed, 6 skipped**, compileall·`git diff --check`·Ruff fatal 규칙(E9/F63/F7/F82) 통과, 독립 read-only 리뷰 PASS였다. 이는 `.proofline/lines/line-0001/implementation-progress.md`와 `implementation-complete.md`에 원문 댓글로 보존한다.
## Scope
### 목록 응답 additive 필드
- `PortfolioItem`에 다음 additive 필드를 제공한다.
- `net_contributed_capital: float = 0.0`
- `cumulative_profit_withdrawn: float = 0.0`
- API·문서·소비자 표시 명칭과 의미를 다음과 같이 통일한다.
- `net_contributed_capital`: **순투자현금** — 현재까지 외부에서 순유입된 투자 현금, `유효 INVEST 합계 - 유효 WITHDRAW 합계`.
- `cumulative_profit_withdrawn`: **누적 수익 인출액** — 유효 `PROFIT`으로 기록되어 인출된 수익금 누계이며 매매상 실현손익 전체를 뜻하지 않는다.
- manual ledger가 없는 포트폴리오는 두 값을 `0.0`으로 반환한다.
- 기존 필드를 제거하거나 의미를 변경하지 않는 additive API 확장으로 제공한다.
### 다중 포트폴리오 집계 repository 계약
- 목록 route가 실제로 반환할 portfolio ID 집합을 입력받아 portfolio ID별 `net_contributed_capital`과 `cumulative_profit_withdrawn`을 집계한다.
- `source = 'manual'`이고 `side IN ('invest', 'withdraw', 'profit')`인 원장 중 `REVERSAL.target_ledger_entry_id`로 참조되지 않은 유효 원장만 집계한다.
- `REVERSAL` 자체는 투자·회수·수익 금액으로 합산하지 않는다.
- 목록과 단건 summary는 동일한 유효 원장 판정과 집계 구현을 사용한다.
- 여러 포트폴리오는 portfolio ID별로 격리한다.
- 한 번의 grouped aggregate query를 사용하며 portfolio별 N+1 ledger 조회를 금지한다.
- 빈 ID 목록은 DB를 조회하지 않고 빈 결과를 반환한다.
### 목록 route 결합과 가시성
- 기존 owner/service/admin 가시성과 `include_disabled`, `market` 필터를 먼저 적용한다.
- 필터링된 반환 대상 portfolio ID만 집계 repository에 전달한다.
- 목록 assembler가 집계값을 각 `PortfolioItem`에 결합한다.
- 집계 결과에 없는 portfolio ID는 두 값을 `0.0`으로 채운다.
- 기존 client token 거부, service token daemon scope, session owner scope, admin `user_id` scope, `include_disabled`, `market` 동작을 유지한다.
- owner/admin/service 또는 필터 범위 밖의 ledger를 집계하거나 노출하지 않는다.
- 빈 목록은 `portfolios: []`, `total: 0`을 정상 반환한다.
### 단건 summary 호환성
- `GET /v1/portfolios/{portfolio_id}/returns/summary`를 유지한다.
- 목록과 단건 endpoint는 동일한 유효 원장 판정과 `INVEST`, `WITHDRAW`, `PROFIT` 산식을 사용한다.
- `PROFIT`을 `WITHDRAW`에 합산하지 않는다.
- `REVERSAL`된 원거래는 `total_invested`, `total_withdrawn`, `net_contributed_capital`, `cumulative_profit_withdrawn`에서 제외한다.
- 기존 이중 reversal 거부 동작을 유지한다.
### 문서와 소비자 계약
- OpenAPI schema와 interface/API 문서에 목록의 additive 필드, 산식, REVERSAL 정책을 반영한다.
- 두 필드는 `portfolio-hub` ledger 권위 집계이며 `trex-web`이 재계산하지 않는다.
- `portfolio-hub` 변경을 먼저 제공한 뒤 `trex-web`이 새 필드를 필수 소비한다.
- `trex-web`은 `net_contributed_capital`을 투자금액으로 사용한다.
### Acceptance admission
기존 ProofLine AC가 없는 초기 project이므로 이 Line은 다음 version-independent product behavior를 새 AC로 `create`한다. 과거 구현 결과·PR·테스트 횟수는 AC가 아니라 Evidence와 비정식 증적 문서에 보존한다.
- 목록 응답의 additive 투자금 집계와 ledger 없음 기본값
- REVERSAL-aware manual ledger 산식과 목록·단건 의미 일치
- grouped aggregate와 N+1 방지 및 빈 입력 무조회
- 기존 권한·필터 이후 반환 대상만 집계하는 격리
- additive 호환성, 문서 의미, 기존 이중 reversal 거부 회귀
## Out of Scope
- `trex-web` 저장소의 소비자 UI 구현 자체는 이 repository-local Line에서 변경하지 않는다.
- 배포, 운영 반영, 릴리스 승인 또는 실제 PostgreSQL 운영 데이터 변경은 승인하지 않는다.
- `PROFIT`을 매매상 실현손익 전체로 재정의하지 않는다.
- 단순 역사 누계가 별도 지표로 필요하더라도 기존 두 필드의 의미를 변경하지 않는다.
- 행별 단건 summary 호출 또는 소비자 측 ledger 재해석을 대안으로 허용하지 않는다.
- 이 사후 문서화는 #23 구현이 ProofLine 정식 시퀀스로 수행됐다는 주장, 공식 delivery closure 또는 배포 완료 주장을 만들지 않는다.
## Risks and Unknowns
- 실제 PostgreSQL integration fixture로 `ANY(%s)` 배열 바인딩, NUMERIC aggregate, REVERSAL row 동작을 직접 입증하는 보강은 당시 독립 리뷰의 비차단 Low 권고로 남았다.
- 운영 규모에서 reversal anti-join의 `EXPLAIN (ANALYZE, BUFFERS)`를 관찰하고 필요하면 partial index를 검토하는 성능 후속이 남았다.
- 위 두 항목은 승인된 필드 의미나 현재 구현 범위를 변경하지 않는 후속 검증·운영 관찰 사항이다.