Files
portfolio-hub/.proofline/lines/line-0001/dcy-0001.md
T

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

원문과 승인 근거

당시 계약

  • 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를 검토하는 성능 후속이 남았다.
  • 위 두 항목은 승인된 필드 의미나 현재 구현 범위를 변경하지 않는 후속 검증·운영 관찰 사항이다.