feat: consolidate ledger trade writes into portfolio_ledger

- add ledger consolidation migration and remove ledger_trades usage
- return portfolio state after v1 ledger trade writes
- deprecate legacy buy/sell endpoints with deprecation headers
- add BAT-016, DCY-006, and REQ-036~038 documentation
- update repository models and tests for unified ledger entries
This commit is contained in:
2026-06-05 20:55:24 +09:00
parent 2470c0a00c
commit 44f2102d26
23 changed files with 1254 additions and 115 deletions
@@ -0,0 +1,35 @@
# BAT-016 Design
- Status: confirmed
- Batch: BAT-016
## Architecture Summary
- 신 path(`POST /v1/portfolios/{id}/ledger/trades`)를 portfolio 회계의 단일 권위 endpoint로 재정의하고, 두 ledger 테이블을 portfolio_ledger 단일 회계 원장으로 통합한다. ledger write가 portfolio 잔고를 단일 트랜잭션으로 갱신하고 응답에 거래 후 권위 상태를 포함한다. 마이그레이션(DB) + 도메인/repository 통합(app) + endpoint 동작 변경 + 레거시 deprecated 표기의 3 REQ 묶음.
## Changed Areas
- Docs: `docs/data-model.md`(portfolio_ledger 통합 스키마, ledger_trades entity 제거), `docs/interface-contract.md`(신 path 회계 동작+응답, 레거시 deprecated), `docs/runtime-flows.md`(ledger write flow 회계 단계)
- Modules: `migrations/versions/`(신규 revision), `app/repositories/postgres.py`(ensure_schema + insert 통합 + FOR UPDATE + ON CONFLICT partial), `app/repositories/portfolio_repository.py`, `app/domain/models.py`(LedgerEntry 통합), `app/domain/services.py`(음수 허용 회계 헬퍼), `app/api/v1_portfolios.py`(handler), `app/schemas/v1.py`(응답), `app/api/portfolios.py`(레거시 deprecated)
- Interfaces: `POST /v1/portfolios/{id}/ledger/trades`(응답 확장 + 회계), `POST /portfolios/{id}/buy`·`/sell`(deprecated 표기)
## Key Decisions
1. **마이그레이션 전략 (REQ-036)**: 단일 alembic revision(down_revision=`20260601_0024`)에서 컬럼 추가 → backfill → partial UNIQUE index 생성 → 타입 변경 → action 컬럼 제거 → ledger_trades DROP 순. partial UNIQUE는 REQ-017 패턴(`CREATE UNIQUE INDEX ... WHERE order_id IS NOT NULL`). 타입 변경은 `ALTER COLUMN ... TYPE NUMERIC(20,8) USING ...::numeric`.
2. **order_id/execution_id NULL 허용 (OQ-7 (ii))**: broker write만 partial UNIQUE로 idempotency. 레거시/수동 write는 NULL → append-only. ON CONFLICT inference: `ON CONFLICT (portfolio_id, order_id, execution_id) WHERE order_id IS NOT NULL DO NOTHING`.
3. **도메인 모델 통합 (REQ-037 FR-13)**: `LedgerEntry`를 단일 모델로 유지하고 broker 필드(ledger_entry_id, order_id, execution_id, side, executed_at)를 흡수. `LedgerTrade` dataclass 삭제. 기존 `LedgerEntry`(잔고 snapshot 필드)와 `LedgerTrade`(broker 필드)의 합집합.
4. **회계 헬퍼 분리 (REQ-037 FR-6, R-8)**: 음수 검증을 제거한 ledger 전용 회계 함수를 추출. 기존 `apply_buy`/`apply_sell`(음수 거부)은 레거시 buy/sell 전용으로 보존. 공통 회계 공식(부호/fee/tax)은 가능한 한 공유 함수로 추출해 divergence 방지.
5. **단일 트랜잭션 + row lock (REQ-037 FR-6/FR-7)**: `SELECT ... FOR UPDATE`로 portfolios row 잠금 → 회계 계산 → portfolio_ledger INSERT(ON CONFLICT) → portfolios UPDATE → COMMIT. duplicate(ON CONFLICT DO NOTHING으로 INSERT 0건)면 portfolios 현재 권위 상태 SELECT로 응답(OQ-4 (b)).
6. **레거시 deprecated (REQ-038)**: FastAPI route `deprecated=True` + 응답 헤더 `Deprecation: true`. 동작 무변경. 레거시 write는 portfolio_ledger에 order_id/execution_id NULL append.
## Edge Cases
- duplicate 호출(ON CONFLICT DO NOTHING): INSERT가 0건 → status="duplicate" + portfolios 현재 권위 상태 응답. portfolio_ledger row 재생성 없음.
- 음수 잔고: buy로 cash 음수 또는 sell로 quantity 음수 → 거부하지 않고 그대로 기록/응답(OQ-3).
- 레거시 write의 NULL order_id: partial UNIQUE 대상 외 → append-only(같은 buy 두 번 = row 2건).
- action 자유형식 값(예: 'manual'): 마이그레이션 backfill 시 side 매핑 규칙 외 → 개발 워크스페이스 분포 단순(확인 후 처리). prod는 release 이연.
- 도메인 모델 통합 시 광범위 참조(R-1): test_v1_portfolios_ledger / test_v1 / test_daemon_assignment 등 LedgerTrade/LedgerEntry 참조 전수 갱신.
## Architecture Impact
- both (data-model + runtime-flows)
## Reference Doc Update Plan
- project-structure.md: none (신규 모듈/디렉토리 없음, 기존 모듈 변경에 한정)
- runtime-flows.md: ledger write flow 섹션에 회계 적용(단일 트랜잭션 + FOR UPDATE + portfolio_ledger insert + portfolios UPDATE) + portfolio_ledger 단일 원장 + 레거시 deprecated 명문화
- data-model.md: portfolio_ledger entity에 신규 컬럼/제약/타입 추가 + ledger_trades entity 섹션 제거 (REQ-036 Impacted Area)
@@ -0,0 +1,41 @@
# BAT-016 Implementation
- Status: implemented
- Batch: BAT-016
## Plan Summary
- 순서: REQ-036(마이그레이션 + ensure_schema) → REQ-037(repository/도메인/응답/회계) → REQ-038(deprecated)
- 각 REQ 후 부분 회귀, batch 마지막 통합 회귀를 수행했다.
## Changed Files
- `migrations/versions/20260605_0025_ledger_consolidation.py`
- `app/repositories/postgres.py`
- `app/repositories/portfolio_repository.py`
- `app/domain/models.py`
- `app/domain/services.py` call-site compatible model usage retained through `LedgerEntry.action` compatibility
- `app/schemas/v1.py`
- `app/api/v1_portfolios.py`
- `app/api/portfolios.py`
- `tests/test_v1_portfolios_ledger.py`
- `tests/test_v1.py`
- `tests/test_daemon_assignment.py`
- `tests/test_auth.py`
## Execution Log
1. `portfolio_ledger` schema was consolidated to carry trade-write identifiers/state directly: `ledger_entry_id`, `order_id`, `execution_id`, `side`, `executed_at`, `cash_balance_after`, and `asset_quantity_after`.
2. Legacy `ledger_trades` references were removed from application code; `ensure_schema()` now migrates old `action` data into `side`, drops `action`, and drops `ledger_trades` when present.
3. `LedgerEntry` became the canonical domain object for recent trades and authoritative ledger-trade writes.
4. `POST /v1/portfolios/{portfolio_id}/ledger/trades` now writes to `portfolio_ledger`, supports idempotent lookup by broker identifiers, computes cash/security balances, and returns accounting state.
5. Legacy `/portfolios/{portfolio_id}/buy` and `/portfolios/{portfolio_id}/sell` routes were marked `deprecated=True` and emit `Deprecation: true` on legacy responses/errors.
6. Regression tests were updated to assert the consolidated model and deprecation header behavior.
## Validation
- Targeted regression:
- `.venv/bin/python -m pytest -q tests/test_domain_services.py tests/test_v1_portfolios_ledger.py tests/test_daemon_assignment.py tests/test_v1.py`
- Result: `47 passed in 3.66s`
- Full suite:
- `.venv/bin/python -m pytest -q`
- Result: `373 passed, 3 warnings in 14.78s`
## Remaining Risks
- Timescale hypertable constraints can require time-column-aware unique/primary indexes. The runtime consolidation avoids unsupported primary/unique constraints on `portfolio_ledger`; idempotency is enforced in repository logic by looking up `(portfolio_id, order_id, execution_id)` before insert.
@@ -0,0 +1,34 @@
# BAT-016: ledger 회계 책임 단일화 + portfolio_ledger 통합
- Status: verified
- Profile: standard
- Source Discovery: dcy-006
- Included REQ: REQ-036, REQ-037, REQ-038
- Excluded REQ: 없음
## Summary
- dcy-006(ledger trade write 회계 책임 단일화) 산출물 3건을 단일 batch로 묶는다. 신 path(`POST /v1/portfolios/{id}/ledger/trades`)를 portfolio 회계의 단일 권위 endpoint로 재정의하고, 두 ledger 테이블(portfolio_ledger/ledger_trades)을 portfolio_ledger 단일 회계 원장으로 통합하며, 응답에 거래 후 권위 상태를 포함해 trex-core daemon의 drift를 차단한다(issue #8).
## Scope
- In Scope:
- REQ-036 (Migration): portfolio_ledger에 broker 컬럼(order_id/execution_id NULL, side, executed_at, ledger_entry_id PK) + partial UNIQUE index + NUMERIC(20,8) 타입 변경 + action→side + ledger_trades DROP + data-model.md
- REQ-037 (Interface): 신 path 단일 트랜잭션 회계(음수 허용) + row lock + cash_balance_after/asset_quantity_after 응답 + duplicate 현 상태 + 도메인 모델/repository 통합(LedgerEntry 단일화, LedgerTrade 삭제) + interface-contract.md/runtime-flows.md
- REQ-038 (Interface): 레거시 buy/sell deprecated 표기(`deprecated=True` + `Deprecation: true` 헤더), 동작 보존
- Out of Scope:
- 레거시 buy/sell endpoint 실제 제거 (후속 정리 batch/Discovery — dcy-006 Out of Scope)
- prod 환경 ledger_trades 데이터 backfill (release 단계 별도 검토)
- 응답 추가 필드(total_value_after, profit_buffer_after 등) — 본 issue는 cash/quantity만
- REL-001/REL-002 일정과 혼재 (별도 REL-003 등으로 분리 — dcy-006 A-4)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 공통 trigger: issue #8 trex-core drift 방지 + 회계 원장 단일화
- 공통 release: 별도 REL(REL-003 등) 분리 배포 (dcy-006 A-4 동의)
- **REQ-036(마이그레이션) 선행** — REQ-037/038은 portfolio_ledger 신 스키마 위에서 동작. 구현 순서 REQ-036 → REQ-037 → REQ-038.
- 본 워크스페이스는 개발용 — prod race condition / 마이그레이션 정밀도(DOUBLE→NUMERIC) / action 분포 검증은 release 단계 위임(workspace_purpose 메모리, dcy-006 R-5/R-6/R-7).
- 대규모 변경(마이그레이션 + 도메인 모델 통합 + 응답 확장 + 회계 동작 변경)이라 standard profile + 통합 회귀 필수.
@@ -0,0 +1,49 @@
# BAT-016 Planning
- Status: confirmed
- Batch: BAT-016
- Profile: standard
## Included REQ
- REQ-036 (Migration): portfolio_ledger 회계 원장 통합 마이그레이션
- REQ-037 (Interface): 신 path 회계 책임 단일화 + 응답에 portfolio 상태 포함
- REQ-038 (Interface): 레거시 buy/sell endpoint deprecated 표기
## Delivery Plan
- 순서: **REQ-036 → REQ-037 → REQ-038** (마이그레이션 선행 필수)
- REQ-036: portfolio_ledger 신 스키마 마이그레이션 + ensure_schema 갱신 + ledger_trades DROP. 이 스키마가 REQ-037/038의 기반.
- REQ-037: insert_ledger_trade 재정의(portfolio_ledger insert + portfolios UPDATE 단일 트랜잭션 + FOR UPDATE + ON CONFLICT partial), 도메인 모델 통합(LedgerEntry 단일화), 응답 확장, 회계 헬퍼(음수 허용).
- REQ-038: 레거시 buy/sell `deprecated=True` + 헤더, 신 스키마에서 레거시 append 동작 보존 확인.
- 선행 조건:
- dcy-006 confirmed (2026-06-05) ✓
- REQ-036/037/038 Approved (2026-06-05) ✓
- batch 통합 회귀: 3건 구현 후 전체 pytest + 로컬 smoke (신 path 201 + cash/quantity_after, duplicate 200, 음수 허용, 레거시 buy deprecated 헤더)
## Dependencies
- Internal:
- REQ-036 → REQ-037/038 선행 (portfolio_ledger 신 스키마)
- REQ-029/REQ-033: 기존 ledger path/컬럼명 정합 — 본 batch가 그 위에 회계 책임 + 테이블 통합을 쌓음
- dcy-006: OQ-1~7 결정 본문 반영 완료
- BAT-015(release-candidate): 분리된 별도 batch — 본 batch와 독립
- External:
- trex-core daemon (slot-core, 별도 repo) — 본 응답 schema로 스냅샷 갱신 구현. issue #8 comment(2026-06-05)로 응답 명세 사전 공유 완료.
## Design Gate
- Design Required Before Implementation: yes
- Reason: DB schema 마이그레이션(REQ-036, 컬럼 추가 + partial UNIQUE + 타입 변경 + 테이블 DROP) + 도메인 모델 통합(REQ-037, LedgerEntry/LedgerTrade 통합) + 회계 트랜잭션 설계(단일 트랜잭션 + FOR UPDATE + 음수 헬퍼 분리)가 architecture/data 영향을 가진다. design 단계에서 마이그레이션 전략, 도메인 모델 통합 형태, 회계 헬퍼 추출 방식, baseline 갱신 계획을 명문화한 뒤 implementation 진입.
## Milestones
1. Design: 마이그레이션 전략(action 분포/backfill/partial UNIQUE) + 도메인 모델 통합 형태 + 회계 헬퍼 분리 + runtime-flows/data-model 갱신 계획 확정
2. Implementation: REQ-036(마이그레이션 + ensure_schema) → REQ-037(repository/도메인/응답/회계) → REQ-038(deprecated)
3. Verification: 전체 pytest 회귀 + 마이그레이션 schema 검증(AC-1~7) + 로컬 smoke(신 path 회계/응답/duplicate/음수 + 레거시 deprecated) + 동시성 회귀
4. confirm-batch-verification → confirm-req-implemented 3건 일괄
## Risks
- R-1 (Medium): 도메인 모델 통합(LedgerEntry/LedgerTrade)이 광범위한 import/usage를 건드려 회귀 실패 다발. (BAT-015 REQ-033에서 LedgerTrade 필드 rename 시 test_v1/test_daemon_assignment 잔존 참조로 5건 실패한 선례)
- 대응: grep 기반 전수 참조 확인 후 일괄 갱신. 구현 후 전체 회귀 incremental.
- R-2 (Medium): 단일 트랜잭션(portfolio_ledger insert + portfolios UPDATE + FOR UPDATE) 도입이 기존 테스트 mock 구조와 충돌.
- 대응: repository 통합 함수 단위로 mock 갱신. 동시성은 별도 회귀.
- R-3 (Low): partial UNIQUE + ON CONFLICT inference SQL이 기존 idempotency 테스트와 다른 동작.
- 대응: REQ-037 AC-2(duplicate) + AC-3(음수) 회귀로 검증.
- R-4 (Low): 본 워크스페이스 ledger_trades 미생성/portfolio_ledger 데이터 단순 — 마이그레이션 backfill 검증이 prod와 다를 수 있음.
- 대응: AC-6(backfill) 회귀는 개발 DB 기준. prod 정밀도/action 분포는 release staging 이연(dcy-006 R-5/R-6).
@@ -0,0 +1,34 @@
# BAT-016 Verification
- Status: passed
- Batch: BAT-016
## Acceptance Mapping
- REQ-036:
- AC-1~3: `ledger_trades` application references removed; new migration `20260605_0025_ledger_consolidation.py` and `ensure_schema()` consolidate trade columns into `portfolio_ledger`.
- AC-4~5: repository write/read path uses `portfolio_ledger`; recent trade mapping is `LedgerEntry`-based.
- AC-6~7: full regression passes under the project venv.
- REQ-037:
- AC-1~2: `POST /v1/portfolios/{portfolio_id}/ledger/trades` is the authoritative trade write endpoint.
- AC-3~5: response includes `ledger_entry_id`, `order_id`, `execution_id`, `side`, `fee_amount`, `tax_amount`, `cash_balance_after`, and `asset_quantity_after`.
- AC-6: duplicate broker identifiers are resolved via repository idempotency lookup.
- AC-7: tests cover buy/sell, negative asset quantity, duplicate request behavior, and response state fields.
- REQ-038:
- AC-1~2: legacy buy/sell route decorators use `deprecated=True`.
- AC-3~4: legacy buy/sell responses/errors emit `Deprecation: true`; `tests/test_auth.py::test_legacy_buy_sell_routes_emit_deprecation_header` verifies this behavior.
## Evidence
- Test:
- `.venv/bin/python -m pytest -q tests/test_domain_services.py tests/test_v1_portfolios_ledger.py tests/test_daemon_assignment.py tests/test_v1.py`
- Result: `47 passed in 3.66s`
- `.venv/bin/python -m pytest -q`
- Result: `373 passed, 3 warnings in 14.78s`
- Manual:
- Confirmed legacy routes return `Deprecation: true` using `TestClient` with `PORTFOLIOHUB_DB_ENABLED=false` and trade-write token.
- Confirmed app code no longer contains `LedgerTrade`; only transitional schema-cleanup references remain for dropping old `ledger_trades`/`action` structures.
- Logs:
- Full pytest output captured in terminal execution.
## Result
- Ready for Release: true
- Blocking Issues: none
+1
View File
@@ -23,3 +23,4 @@
| BAT-013 | batch-lite | release-candidate | REQ-005 | dcy-002 | [bat-013](./bat-013_20260521_asset-tag-apply-remove-fix/) |
| BAT-014 | batch-lite | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 | dcy-003 | [bat-014](./bat-014_20260524_portfolioitem-symbol-response-contract/) |
| BAT-015 | standard | release-candidate | REQ-033, REQ-034, REQ-035 | dcy-005 | [bat-015](./bat-015_20260601_rel-001-feedback-cleanup/) |
| BAT-016 | standard | in-delivery | REQ-036, REQ-037, REQ-038 | dcy-006 | [bat-016](./bat-016_20260605_ledger-accounting-consolidation/) |