9.2 KiB
id, status
| id | status |
|---|---|
| dcy-0001 | 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_investedtotal_withdrawnnet_contributed_capitalcumulative_profit_withdrawn
- PostgreSQL 산식은 manual ledger에서
INVEST,WITHDRAW,PROFIT을 분리했다.net_contributed_capital = INVEST - WITHDRAWcumulative_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.pyapp/repositories/portfolio_repository.pyapp/repositories/postgres.pyapp/api/v1_portfolios.pydocs/interface-contract.mddocs/runtime-flows.md
- 회귀 경로:
tests/test_portfolio_ledger_aggregates.pytests/test_v1_portfolios_list.pytests/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.0cumulative_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_idscope,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-hubledger 권위 집계이며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를 검토하는 성능 후속이 남았다. - 위 두 항목은 승인된 필드 의미나 현재 구현 범위를 변경하지 않는 후속 검증·운영 관찰 사항이다.