fix(portfolios): replay corporate-action cost basis (#38)

This commit is contained in:
2026-09-19 17:07:14 +09:00
committed by GitHub
parent f79f3c79b5
commit 567d662913
12 changed files with 352 additions and 61 deletions
+16
View File
@@ -0,0 +1,16 @@
---
id: "ac-0038"
status: active
---
# 기업행동을 반영한 holdings 원가의 정본성과 불완전 metadata 차단
## Criterion
Portfolio owner 또는 허가된 daemon이 holdings cost-basis를 조회할 때, 시간순 ledger replay에 `stock_consolidation`이 있으면 응답은 기업행동 감사 기록의 양수 `quantity_after`를 현재 수량으로 적용하고 `total_cost_basis`를 authoritative 총원가로 보존하며 주당 원가를 `total_cost_basis / quantity_after`로 산출해야 한다. 필수값인 양수 `quantity_after` 또는 `total_cost_basis`가 없으면 기업행동 전 WAC나 snapshot 값으로 원가를 추정하지 않고 두 원가 필드를 `null`로 반환하며 기업행동 metadata 부족을 기존 warning 필드에서 구분해야 한다. 기업행동 이후 거래는 같은 replay에서 반영되고, 기업행동이 없는 기존 BUY/SELL 원가·warning·권한·응답 구조는 유지되어야 한다.
## Verification
- 성공 조건: BUY/SELL 뒤 `1,452 → 726` consolidation을 구성하면 `current_quantity=726`, `total_cost_basis=1,577,211.97990000`, `cost_basis_per_unit=total_cost_basis/726`과 null warning을 반환한다. 저장된 8자리 `average_price_after`를 곱하여 총원가를 재구성하지 않으며, 후속 BUY/SELL 또는 기업행동이 있으면 정렬된 replay 결과가 현재 원가에 반영된다.
- 실패·경계 조건: `quantity_after` 누락·0 이하 또는 `total_cost_basis` 누락에서는 `cost_basis_per_unit`과 `total_cost_basis`가 모두 null이고 기업행동 metadata 부족 warning이 non-null이어야 한다. 기업행동 전 WAC를 정상 원가로 노출하거나 BUY/SELL-only 결과, 기존 불완전 거래 warning, response field/nullability, authorization, cash·profit buffer를 변경하면 FAIL이다.
- 검사 방법: domain unit test, holdings API fixture와 실제 PostgreSQL joined-ledger integration으로 metadata 전달·순서·정상/실패 결과를 검사한다. 기존 BUY/SELL·authorization·corporate-action atomicity 회귀와 전체 test suite, 정적 검사를 실행하고 기존 trex-core consumer가 정상 응답의 수량·단가·총원가 일관성을 통과하는지 별도 호환성 probe로 확인한다.
+2 -2
View File
@@ -1,5 +1,5 @@
{
"schema_version": 1,
"next_line_number": 8,
"next_ac_number": 38
"next_line_number": 9,
"next_ac_number": 39
}
+48
View File
@@ -0,0 +1,48 @@
---
id: "dcy-0008"
status: confirmed
---
# 기업행동 원가를 WAC replay에 반영 (#37)
## Problem
기업행동이 기록된 portfolio에서 `GET /v1/portfolios/{portfolio_id}/holdings/cost-basis`가 기업행동 후 수량과 기업행동 전 WAC를 섞어 반환한다. `019685` 사례에서 portfolio snapshot 수량은 `726`이지만 endpoint는 병합 전 단가 `1,086.2341459366392`를 사용하여 총원가를 `788,605.9899500001`로 계산한다. 기업행동 감사 기록은 병합 후 수량 `726`과 authoritative `total_cost_basis=1,577,211.97990000`을 이미 보유한다. 이 불완전 응답은 downstream의 안전 gate를 활성화하여 자동 reconcile을 중단시킨다.
## Evidence
- `E-001` Source issue `https://github.com/fourblocks2020/portfolio-hub/issues/37`은 운영 사례, 확인된 원인, 수정 사양과 acceptance criteria를 기록한다. 확인 시점의 issue body SHA-256은 `79acbcd09e22c309a8438efde32ff12b9414815557e28ad36802a71c9200bdec`, `updated_at`은 `2026-09-19T02:20:12Z`다.
- `E-002` 기준 source는 `portfolio-hub` `f79f3c79b5e9c7e13fb28260af71e7e56cf575f5`다. `app/repositories/postgres.py::get_all_trades_for_pnl()`은 기업행동 metadata를 `LedgerEntry.corporate_action`에 보존하며 `executed_at ASC, ledger_entry_id ASC` 순서로 반환한다.
- `E-003` `app/api/v1_portfolios.py::get_holdings_cost_basis()`는 ledger entry를 domain replay row로 변환할 때 `side`, `quantity`, `price`, `fee_amount`, `executed_at`만 전달하므로 `corporate_action` metadata를 버린다. 이후 portfolio snapshot 수량과 `calculate_current_wac()`의 unit WAC를 곱하여 총원가를 다시 만든다.
- `E-004` `app/domain/pnl.py::calculate_current_wac()`에는 `stock_consolidation` 분기가 이미 있으나 metadata가 없으면 running state에서 값을 추정하는 fallback이 있고, 반환형 `CurrentWacResult`는 authoritative total을 보존하지 않는다.
- `E-005` `app/schemas/v1.py::HoldingsCostBasisResponse`는 `cost_basis_per_unit`과 `total_cost_basis`를 이미 nullable로 정의하므로 신규 wire field나 DB migration 없이 fail-closed 응답을 표현할 수 있다.
- `E-006` `docs/interface-contract.md`는 stock consolidation의 `total_cost_basis`를 authoritative pre-action acquisition cost로 정의하고, persisted 8-decimal WAC의 반올림 residue가 정상임을 명시한다.
- `E-007` active `AC-0037`은 joined repository read와 corporate-action metadata mapping 보존을 소유하지만, `req-0007`은 WAC·원가 회계 규칙 변경을 명시적으로 제외한다. Authoritative total을 holdings 응답까지 보존하는 동작은 별도의 active AC가 필요하다.
## Scope
- `S-001` holdings endpoint가 `LedgerEntry.corporate_action`의 `quantity_before`, `quantity_after`, `total_cost_basis`와 식별·순서 metadata를 domain replay row에 전달하도록 한다. 이는 existing `AC-0037`을 `satisfy`한다.
- `S-002` 유효한 `stock_consolidation`을 replay할 때 running quantity를 `quantity_after`로 바꾸고, authoritative `total_cost_basis`를 보존하며, `cost_basis_per_unit = total_cost_basis / quantity_after`로 계산한다. 이 endpoint 원가 계약은 지속적인 독립 검증을 위해 새 AC를 `create`한다.
- `S-003` `quantity_after`가 없거나 0 이하이거나 `total_cost_basis`가 없으면 기업행동 전 WAC로 추정하지 않는다. holdings 응답의 두 원가 필드를 `null`로 반환하고 기존 `data_completeness_warning` 필드에 기업행동 metadata 부족을 구분하는 안정적인 문자열을 반환한다. 신규 response field는 만들지 않는다.
- `S-004` 정상 기업행동, metadata 부족, 기업행동 이후 BUY/SELL, BUY/SELL-only 회귀와 API metadata 전달을 domain·API·PostgreSQL integration 수준에서 검증한다. 기존 trex-core source를 변경하지 않고 consumer compatibility를 검증한다.
- `S-005` interface contract에 holdings cost-basis의 기업행동 원가 출처와 계산식을 기록한다.
## Out of Scope
- `OS-001` trex-core source·허용오차·slot 상태 변경, broker 주문, B warning 삭제 또는 production data 보정.
- `OS-002` `0.1%`, `1,000원 상한`, 소수점 8자리 일치 같은 추가 tolerance 정책.
- `OS-003` corporate-action write transaction, idempotency, recovery, cash, profit buffer, portfolio reference price 또는 DB schema 변경.
- `OS-004` holdings 응답에 신규 reason field를 추가하거나 기존 BUY/SELL-only WAC 계약을 변경하는 일.
## Risks and Unknowns
### Risks
- `R-001` 현재 WAC domain은 `float`를 사용한다. 저장된 8자리 `average_price_after`를 authoritative total 재구성에 사용하면 반올림 residue가 생기므로, total은 별도 replay state로 보존하고 endpoint가 그 값을 그대로 반환해야 한다.
- `R-002` 기업행동 metadata 부족 상태에서 기존 WAC를 남기면 warning과 함께 잘못된 원가가 다시 노출될 수 있다. unusable 상태를 결과형에서 명시적으로 표현하고 두 원가 필드를 함께 `null`로 만드는 회귀가 필요하다.
- `R-003` 기업행동 이후 BUY/SELL이 있으면 authoritative total도 거래와 함께 증감해야 한다. unit WAC와 quantity만 갱신하고 total을 고정하면 후속 거래가 있는 portfolio에서 잘못된 값을 반환하므로 running total과 unit WAC의 관계를 함께 검증해야 한다.
### Assumptions
- `A-001` Issue #37에 기록된 정상 동작과 비범위가 이번 delivery의 제품 결정이다. Discovery confirmation에서 이 scope를 canonical하게 확정한다.
- `A-002` repository가 제공하는 `executed_at ASC, ledger_entry_id ASC` 순서를 endpoint에서 재정렬하지 않고 그대로 replay한다.
@@ -0,0 +1,5 @@
# Activity Log
- 2026-09-19T11:36:22+09:00 — 사용자가 `dcy-0008`을 confirmed로 확정하고 exact `req-0008`·`ac-0038`을 승인했다. `req-0008`은 approved, `ac-0038`은 active로 전환했고 `proofline validate`가 통과했다(legacy line status warning만 존재).
- 2026-09-19T11:36:22+09:00 — 구현 계획: API replay row에 기업행동 metadata를 보존하고, WAC replay에서 authoritative total과 post-action quantity를 적용하며, 필수 metadata 누락을 fail-closed로 처리한다. Domain/API/PostgreSQL/consumer 회귀와 기존 BUY/SELL 동작을 검증한다.
- 2026-09-19T11:54:52+09:00 — 구현 및 검증: 기업행동 metadata 전달, `total_cost_basis / replay quantity` 계산, 누락 metadata의 원가 `null`/warning 처리를 구현했다. 표적 테스트 66건, 전체 테스트 631건(25 skip), PostgreSQL 기업행동 모듈 6건, Ruff ratchet, compileall, dependency check, ProofLine validation이 통과했다. metadata 전달을 제거한 sabotage에서 신규 API 회귀가 실패하고 복원 후 통과했다. `trex-core` 실제 reconciliation 경로는 `auto_synced`로 호환됨을 확인했다. 다음 단계는 독립 구현 리뷰와 PR delivery다.
+4
View File
@@ -0,0 +1,4 @@
---
id: "line-0008"
status: discovery
---
+40
View File
@@ -0,0 +1,40 @@
---
id: "req-0008"
status: approved
discovery: "dcy-0008"
criteria:
create:
- "ac-0038"
update: []
retire: []
satisfy:
- "ac-0037"
---
# 기업행동 원가를 WAC replay에 반영 (#37)
## Objective
기업행동이 포함된 portfolio의 holdings cost-basis가 기업행동 감사 기록의 authoritative 총원가와 기업행동 후 수량을 사용하여 현재 원가를 반환하고, 계산에 필요한 metadata가 없을 때 병합 전 원가를 정상값처럼 노출하지 않도록 한다.
## Scope
- `GET /v1/portfolios/{portfolio_id}/holdings/cost-basis` replay에 repository가 결합한 `stock_consolidation` metadata를 전달한다.
- 정상 기업행동은 `quantity_after`를 현재 replay 수량으로 적용하고 감사 기록의 `total_cost_basis`를 authoritative 총원가로 보존하며, 주당 원가는 `total_cost_basis / quantity_after`로 계산한다.
- 기업행동 이후 BUY/SELL 또는 후속 기업행동이 있으면 같은 시간순 ledger replay 안에서 현재 수량·주당 원가·총원가를 계속 계산한다.
- 필수 기업행동 metadata인 양수 `quantity_after` 또는 `total_cost_basis`가 없으면 기업행동 전 WAC로 추정하지 않고 holdings 응답의 `cost_basis_per_unit`과 `total_cost_basis`를 `null`로 반환하며, 기존 warning 필드로 기업행동 metadata 부족을 구분한다.
- 정상·불완전 기업행동, 기업행동 이후 거래, BUY/SELL-only, authorization, ordering과 consumer compatibility를 회귀 검증하고 interface contract를 동기화한다.
## Constraints
- 기업행동 감사 기록의 `total_cost_basis`가 원가 총액의 정본이다. Persisted `average_price_after`를 곱하여 총원가를 재구성하거나 별도의 허용오차 판정에 사용하지 않는다.
- `HoldingsCostBasisResponse`의 기존 필드와 nullability를 유지하며 신규 reason field 또는 DB migration을 추가하지 않는다.
- Repository의 `executed_at ASC, ledger_entry_id ASC` 순서, daemon scope와 portfolio ownership 검증을 유지한다.
- 기업행동이 없는 BUY/SELL-only portfolio의 WAC 및 기존 불완전 거래 이력 warning 의미를 변경하지 않는다.
- trex-core source와 검증 tolerance를 변경하지 않는다.
## Non-Goals
- `0.1%`, `1,000원 상한`, 소수점 8자리 일치 등 기업행동 원가의 추가 tolerance 정책.
- Corporate-action write transaction, idempotency, recovery, cash, profit buffer, portfolio reference price 또는 기존 ledger row의 수정·backfill.
- Broker 주문, trex-core slot 변경, B warning 삭제, production rollout 또는 특정 production portfolio 보정.