Files
portfolio-hub/.proofline/lines/line-0006/evidence/issue-29-approved-discovery.md
T

10 KiB

배경

trex-web 포트폴리오 상세 화면은 현재 다음 manual 전용 계약만 사용합니다.

GET /v1/portfolios/{portfolio_id}/ledger/manual?limit=50

이 계약은 수동으로 기록한 원장만 표시하므로, broker에서 유입된 체결/원장 이벤트를 포함한 포트폴리오의 통합 원장 화면을 제공할 수 없습니다.

현재 provider 확인 근거:

  • app/api/v1_portfolios.py의 manual 목록 route는 require_read 뒤에 user/session token 및 portfolio owner를 확인한다.
  • app/repositories/postgres.py의 list_manual_ledger_entries()는 WHERE portfolio_id = %s AND source = 'manual'로 필터하고 recorded_at DESC, ledger_entry_id DESC로 정렬한다.
  • 도메인 LedgerEntry는 source를 보유하며 기본값은 broker; manual 기록은 source='manual'이다.
  • 현재 응답의 total은 전체 일치 건수가 아니라 반환된 items 길이이다.

목표

포트폴리오 소유자가 manual과 broker를 포함한 모든 출처의 원장 기록을 하나의 안정적이고 page 가능한 read 계약으로 조회할 수 있게 한다. 소비자 UI는 owner가 제공한 원장 사실을 표시할 뿐, 원장 action이나 잔고 효과를 재계산하지 않는다.

제안 API 초안

GET /v1/portfolios/{portfolio_id}/ledger?limit=50
GET /v1/portfolios/{portfolio_id}/ledger?limit=50&cursor=...
GET /v1/portfolios/{portfolio_id}/ledger?source=manual&limit=50
GET /v1/portfolios/{portfolio_id}/ledger?source=broker&limit=50
  • source 미지정은 모든 출처를 최신순으로 반환한다.
  • source가 지정되면 비어 있지 않은 provenance 문자열을 exact match로 사용한다. 현재 결과가 없는 값도 오류가 아니라 빈 page(items=[], next_cursor=null)를 반환한다.
  • 기본 정렬은 recorded_at DESC, ledger_entry_id DESC이며, cursor는 두 값을 함께 고정해 동률에서도 중복/누락 없이 다음 페이지를 반환해야 한다.
  • 응답 items는 적어도 ledger_entry_id, portfolio_id, source, canonical 대문자 action, recorded_at, executed_at(있는 경우), order/execution 식별자(있는 경우), 수량·가격·수수료·세금·note, 기록 후 owner snapshot, reversal 참조와 confirmation evidence를 표현해야 한다.
  • 새 통합 endpoint는 side를 노출하지 않고 canonical 대문자 action만 노출한다. 공통 필드와 출처별 필드는 response schema에 명시하며, 해당 사실에 값이 없을 때만 null을 반환한다. broker 원장과 manual 원장의 필드 공백을 0이나 추정값으로 채우지 않는다.

Open Questions

각 OQ에서 하나의 선택지를 선택해 주세요. 선택은 Discovery 결정을 위한 것이며 구현·push·배포 승인을 뜻하지 않습니다.

OQ-001 — pagination 계약

  • A. Cursor pagination — recorded_at과 ledger_entry_id 기반 opaque cursor, next_cursor 반환.
  • B. Offset pagination — offset과 limit으로 페이지 이동.
  • C. 단일 무제한 응답 — 모든 원장을 한 응답에 반환.

권장안: A — 원장은 계속 증가하며, 최신순 목록에 새 행이 들어와도 cursor가 offset보다 중복·누락에 강하다. 무제한 응답은 응답 크기와 DB 부하 경계가 없다.

OQ-002 — source 필터 정책

  • A. 전체 출처가 기본값이며, 선택적 source filter를 제공한다.
  • B. 전체 출처만 제공하며, source filter는 제공하지 않는다.
  • C. manual/broker 전용 endpoint를 계속 분리하고 통합 endpoint는 만들지 않는다.

권장안: A — 통합 운영 화면의 기본 요구를 충족하면서, manual-only 감사 화면 같은 소비자는 같은 안정적 계약에서 명시적으로 좁힐 수 있다. 기존 /ledger/manual은 하위호환성을 위해 유지한다.

OQ-003 — read 권한 경계

  • A. 현재 manual 조회와 동일하게 user/session token + portfolio owner만 허용한다.
  • B. owner 외 admin도 명시적으로 허용한다.
  • C. daemon service token에도 portfolio scope 내 조회를 허용한다.

권장안: A — 최초 소비자인 trex-web의 사용자 상세 조회를 최소 권한으로 충족한다. admin/service 조회는 실제 운영 소비자와 audit 노출 범위를 별도로 확정한 뒤 additive하게 설계하는 편이 안전하다.

OQ-004 — 전체 건수(total) 의미

  • A. next_cursor만 반환하고 전체 건수는 제공하지 않는다.
  • B. 정확한 전체 일치 건수를 total로 반환한다.
  • C. 현재 page의 반환 건수를 total로 반환한다.

권장안: A — 현재 manual endpoint의 total=len(items)와 전역 건수 의미를 혼동하지 않으며, 목록 UI에 필요한 다음 페이지 판단도 충족한다. 정확한 total은 필요성이 확인될 때 별도 비용·일관성 계약으로 추가한다.

OQ-005 — 통합 원장 item의 정규 응답 계약

  • A. 하나의 canonical action 필드를 대문자 enum으로 제공하고, 모든 공통·출처별 필드는 schema에 명시적으로 nullable로 선언한다. — 결정
    • 새 통합 endpoint는 side를 노출하지 않는다.
    • action은 manual/broker 모두에서 동일한 대문자 vocabulary를 사용한다.
    • 값이 없는 source별 사실은 0 또는 추정값으로 대체하지 않고 null로 표현한다.
  • B. action과 side를 모두 노출하고 source별 field 존재·동치·casing 규칙을 명시한다.
  • C. broker와 manual을 별도 item type/discriminator schema로 제공한다.

OQ-006 — cursor scope 및 오류 계약

  • A. opaque cursor는 portfolio_id, 선택된 source filter 및 (recorded_at, ledger_entry_id)를 결속한다. malformed·tampered·다른 portfolio/source에 재사용한 cursor는 HTTP 422로 거부한다. 마지막 page의 next_cursor는 null이다. (권장)
    • 다른 목록 조건의 cursor 혼용을 fail-closed하고, 소비자는 next_cursor is null로 종료를 판정한다.
  • B. cursor에는 (recorded_at, ledger_entry_id)만 담고, portfolio/source 불일치는 구현이 현재 query 결과로 처리한다.
    • cursor 재사용의 의미와 오류가 모호해진다.
  • C. cursor pagination을 유지하되 cursor 오류·scope 동작은 구현 단계에서 정한다.
    • 외부 API의 실패 동작을 구현자 재량으로 남긴다.

OQ-007 — source parameter의 허용값 및 결과 없음 의미

  • A. source는 비어 있지 않은 provenance 문자열을 exact match로 허용하며, 알 수 없는 값은 빈 page(items=[], next_cursor=null)를 반환한다. — 결정
    • all-source 기본값과 미래 producer source를 별도 API 변경 없이 지원한다.
    • source 값은 portfolio owner 권한 검증 뒤에만 평가한다.
  • B. source는 현재 manual·broker enum만 허용하고 그 외 값은 HTTP 422로 거부한다.
  • C. known source 조회는 허용하되, source 목록을 API metadata로 제공한 뒤 그 목록 밖 값의 처리 규칙을 별도 계약으로 정한다.

포함 범위

  • source-중립 통합 원장 read endpoint, request/response schema, repository query와 cursor semantics.
  • portfolio ownership 및 선택된 권한 모델의 명시적 검증.
  • manual과 broker 행 혼합, 동일 recorded_at tie-breaker, page 경계, source filter, token/타인 portfolio 경계에 대한 계약 테스트.
  • 필요한 DB index 및 migration 여부 검토. 실제 PostgreSQL에서 pagination query plan과 migration을 검증한다.
  • OpenAPI/API interface 문서 갱신.
  • 기존 GET /v1/portfolios/{portfolio_id}/ledger/manual 계약의 하위호환성 회귀 테스트.

제외 범위

  • ledger 생성·수정·삭제, REVERSAL 실행 또는 기존 원장의 재해석.
  • broker 주문/체결 생성, strategy signal, outbox/producer 동작 변경.
  • 원장에 없는 경제적 효과·잔고·PnL을 API나 trex-web이 추론하는 기능.
  • portfolio return aggregate 의미 변경(관련 이슈 #28의 별도 범위).
  • trex-web UI 구현, push/PR/merge, 운영 배포·릴리스.

수용 기준 초안

  • OQ-001~007 선택이 issue 본문에 결정으로 기록된다.
  • 소유자가 자신의 portfolio에 대해 선택된 source 범위의 원장을 결정된 정렬·pagination 규칙으로 조회할 수 있다.
  • 서로 다른 출처의 행이 누락 없이 공통 read model로 표현되며, 새 통합 endpoint는 대문자 canonical action만 노출한다. 없는 source별 사실은 추정값이 아닌 schema의 명시된 nullable 값이다.
  • cursor 페이지 경계에서 동일 시각 행의 중복·누락이 없고, cursor의 portfolio/source scope 및 malformed·tampered cursor 거부 동작은 OQ-006 결정과 일치한다.
  • 선택된 권한 범위 밖의 token 또는 타인 portfolio는 기존 정책에 맞는 거부 응답을 받는다.
  • source 필터가 선택되면 지정 provenance와 exact match인 row만 반환하고, 결과 없는 non-empty 값은 빈 page(items=[], next_cursor=null)를 반환한다. 미지정 기본 동작은 OQ-002·OQ-007 결정과 일치한다.
  • /ledger/manual의 source=manual 전용·최신순·limit 계약은 하위호환으로 유지된다.
  • API schema/OpenAPI와 실제 응답이 일치하고, focused 및 전체 회귀 테스트가 통과한다.

관련

  • 소비자 맥락: fourblocks2020/trex-web#15
  • 별도 집계 의미 변경: #28
  • 작성 시각: 2026-08-18T19:04:00+09:00

Discovery 상태: APPROVED — requester가 OQ-001~007을 모두 명시적으로 결정했고, 2026-08-20 KST에 현재 provider/data-flow/ownership 계약과 대조하여 승인했다. 이 승인은 구현·커밋·push·PR·merge·배포·릴리스 승인을 포함하지 않습니다.