chore: commit current workspace changes

This commit is contained in:
2026-06-04 23:38:42 +09:00
parent 13837acc60
commit aed54911d4
20 changed files with 561 additions and 96 deletions
@@ -1,35 +1,52 @@
# BAT-006 Implementation
- Status: draft
- Status: complete
- Batch: BAT-006
## Plan Summary
- LedgerPort 계약과 portfolio-hub/stub adapter 를 추가하고, execution fill 경로가 ledger outbox work item 을 적재하도록 연결한다. retry worker 와 dedup/outcome 처리를 구현한 뒤 baseline 문서를 실제 코드와 맞춘다.
- LedgerPort 계약 + portfolio-hub HTTP 어댑터 + 결정론 stub 원장을 추가하고, 집행 루프 fill 전이 직후에 ledger outbox 에 work item 을 적재하도록 연결했다. `LedgerWriter` 가 outbox 를 drain 해 portfolio-hub write 를 시도하고, NEW/DUPLICATE_OK/conflict/retryable 결과를 `completed/duplicate_completed/retryable_failure/conflict_blocked` outcome 으로 분류한다. paper/KIS 브로커는 결정적 `execution_id` 와 `fee` 를 `FillStatus` 로 운반한다. baseline 4문서를 ledger 레이어로 정합시켰다.
## Changed Files
- 예정:
- `trex_core/application/*` — LedgerPort, ledger outbox service, execution fill hook.
- `trex_core/adapters/ledger/*` — in-memory/stub ledger, portfolio-hub adapter.
- `trex_core/adapters/broker/*` — FillStatus execution_id/fee 운반.
- `trex_core/runtime/*` — portfolio-hub config, retry worker lifecycle.
- persistence/migration 파일 — ledger outbox work item store.
- `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/project-structure.md`, `docs/data-model.md`.
- tests — ledger adapter, fill lifecycle, outbox retry/dedup/conflict, import sweep.
- 신규
- `trex_core/application/ledger_port.py` — `LedgerPort` Protocol, `LedgerTrade`, `LedgerWriteResult`, `LedgerError`/`LedgerConflictError`/`LedgerRetryableError` (REQ-017).
- `trex_core/application/ledger_outbox.py` — `LedgerOutbox` Protocol, `LedgerWorkItem`, `OutboxOutcome`, `WorkItemStatus`, `LedgerWriter`, `InMemoryLedgerOutbox` (REQ-019).
- `trex_core/adapters/ledger/__init__.py`, `in_memory.py` (`InMemoryLedger` stub), `portfolio_hub.py` (`PortfolioHubLedger` HTTP 어댑터) (REQ-017).
- `trex_core/adapters/persistence/sql/002_create_ledger_outbox.sql` — `ledger_outbox` 테이블, `correlation_id` PK, status/outcome CHECK (REQ-019).
- `tests/adapters/ledger/{__init__,test_in_memory_ledger,test_portfolio_hub_ledger}.py` — REQ-017 AC-1/2/3.
- `tests/adapters/persistence/test_postgresql_ledger_outbox.py` — REQ-019 PG outbox (PG-gated).
- `tests/application/{test_execution_ledger,test_ledger_outbox}.py` — REQ-018 + REQ-019.
- `tests/domain/test_domain_import_sweep.py` — REQ-017 AC-4 (NFR-1) 정적 import 스윕.
- 수정
- `trex_core/application/broker_port.py` — `FillStatus.execution_id/fee/tax` 확장 + 공유 `calculate_fee(side, amount)` 헬퍼 (REQ-018).
- `trex_core/adapters/broker/paper.py` — `filled` 상태에서 `paper:{broker_order_id}:{filled_qty}` execution_id + fee 합성.
- `trex_core/adapters/broker/kis.py` — `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}` execution_id + fee 운반.
- `trex_core/application/execution.py` — `ExecutionService` 가 fill 전이 직후 `LedgerWorkItem` 을 outbox 에 enqueue, `portfolio_id` 부재면 skip, slot 전이 우선 보장 (REQ-018 + REQ-019).
- `trex_core/adapters/persistence/postgresql.py` — `PostgreSQLLedgerOutbox` (`enqueue`/`list_pending`/`get`/`mark_outcome`).
- `trex_core/runtime/config.py` — `PortfolioHubConfig`, `DaemonConfig.portfolio_hub`/`ledger_interval_sec`, env 로딩.
- `trex_core/runtime/daemon.py` — `_build_ledger`, ledger writer 백그라운드 루프, `ExecutionService(..., ledger_outbox=outbox)`.
- `tests/conftest.py`, `tests/adapters/persistence/test_migration_and_drift.py` — `ledger_outbox` 테이블 반영.
- `.env.example` — `TREX_PORTFOLIO_HUB_BASE_URL/TOKEN/TIMEOUT`, `TREX_LEDGER_INTERVAL_SEC` 추가.
## Execution Log
1. 대기: `draft-batch-planning`/`draft-batch-design` 확인 후 구현 시작.
1. baseline 테스트 88 passed / 8 skipped 확인.
2. REQ-017: `LedgerPort` + `LedgerTrade` + 예외 + `InMemoryLedger` stub + `PortfolioHubLedger` 어댑터 추가, contract 테스트 4종 작성.
3. REQ-018: `FillStatus.execution_id/fee/tax` 확장, paper/KIS 결정적 합성 + `calculate_fee` 헬퍼, `ExecutionService._on_filled` 에서 fill 전이 직후 ledger outbox enqueue, `portfolio_id` 부재 시 skip + `ExecutionResult.ledger_enqueued/ledger_skipped` 카운터 추가.
4. REQ-019: `LedgerOutbox` Protocol + `LedgerWorkItem`/`OutboxOutcome`/`WorkItemStatus`, `LedgerWriter` (correlation_id 로컬 dedup + 어댑터 `LedgerWriteResult.DUPLICATE_OK` 매핑 → `duplicate_completed`), `InMemoryLedgerOutbox`, `PostgreSQLLedgerOutbox`, `002_create_ledger_outbox.sql` 마이그레이션.
5. 데몬 lifespan 에 ledger writer thread 추가, `PortfolioHubConfig` env 로딩.
6. REQ-020: runtime-flows / interface-contract / project-structure / data-model 4문서를 ledger 레이어로 정합.
7. 정적 import 스윕 테스트(`tests/domain/test_domain_import_sweep.py`) 로 `trex_core/domain` 의 ledger/HTTP 의존 0 검증.
8. 최종 테스트: 109 passed / 12 skipped (PG·KIS 게이트). ruff lint 0 issues.
## Validation
- 예정:
- stub ledger `record_trade` 단위테스트.
- portfolio-hub adapter request contract + 409 mapping 테스트.
- buy/sell fill lifecycle 통합테스트.
- duplicate execution_id 중복 0 테스트.
- retryable failure 후 eventually completed 테스트.
- `conflict_blocked` 자동 재시도 중단 테스트.
- `trex_core/domain` import sweep.
- baseline 문서-코드 정합 리뷰.
- 명령: `.venv/bin/python -m pytest -q`
- 결과: `109 passed, 12 skipped, 1 warning`.
- 명령: `.venv/bin/ruff check trex_core tests`
- 결과: `All checks passed!`
- 추가 테스트 21개(REQ-017 7 / REQ-018 3 / REQ-019 7 / NFR-1 import 스윕 1 / PG outbox 4 PG-skip).
- portfolio-hub 어댑터 contract 는 `httpx.MockTransport` 로 path/body/auth/409/5xx/network-error 모두 커버.
- buy/sell lifecycle 테스트로 슬롯 전이와 ledger 기록 분리, dedup, 재시도 결국 기록, 409 무한루프 방지, ledger 장애 시 슬롯 전이 우선 보장을 확인.
## Remaining Risks
- portfolio-hub 실제 계약/자격증명 미가용 시 external integration evidence 가 stub/contract 수준으로 제한될 수 있다.
- outbox persistence schema 가 기존 repository transaction 경계와 맞지 않으면 design 조정이 필요하다.
- portfolio-hub 의 실제 4xx 응답 본문(특히 `duplicate` 플래그) 계약은 orbit 패턴을 가정한다. 실통합 시 응답 본문이 다르면 `PortfolioHubLedger.record_trade` 의 `LedgerWriteResult.DUPLICATE_OK` 판별을 조정해야 한다. 본 batch 는 stub/contract 수준 evidence 로 검증했다.
- KIS `execution_id` 합성에 사용되는 `ord_dt`/`ord_gno_brno` 가 일부 KIS 응답에서 비어 있으면 결정성이 약화될 수 있다. 현재는 누락 시 `today`/빈 문자열로 폴백한다. 운영 데이터 관찰 후 보강 여부 판단(후속 후보).
- 부분체결(partially_filled) 단계에서는 ledger 기록을 enqueue 하지 않는다. 부분체결 누적 fill 의 정밀 회계는 후속 Discovery 범위로 남는다.
@@ -1,6 +1,6 @@
# BAT-006: portfolio-hub ledger 통합
- Status: draft
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-004
- Included REQ: REQ-017, REQ-018, REQ-019, REQ-020
@@ -1,6 +1,6 @@
# BAT-006 Verification
- Status: draft
- Status: approved
- Batch: BAT-006
## Acceptance Mapping
@@ -27,14 +27,45 @@
- AC-3: baseline 문서 변경 이력에 REQ-017~020 반영 기록 evidence.
## Evidence
- Test:
- 대기: 구현 후 테스트 결과 기록.
- Manual:
- 대기: baseline 문서-코드 정합 리뷰 기록.
- Logs:
- 대기: 필요 시 portfolio-hub adapter contract 또는 skip/blocker 로그 기록.
### Test
- 명령: `.venv/bin/python -m pytest -q`
- 결과: `109 passed, 12 skipped, 1 warning in 0.98s` (PG 4건 + KIS 1건 + 기존 PG 7건은 DSN/credentials 게이트로 skip).
- REQ-017 AC-1: `tests/adapters/ledger/test_in_memory_ledger.py::test_record_trade_preserves_fields_and_is_idempotent_on_execution_id` — 단일 호출 후 `count()==1` + 필드 보존, 동일 execution_id 재호출 후에도 `count()==1`.
- REQ-017 AC-2: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_post_contract_includes_execution_id_and_path_uses_asset_id` — `httpx.MockTransport` 로 path `/v1/assets/PF-77/ledger/trades`, body 에 `execution_id`/`side` 포함, `Authorization: Bearer …` 헤더 캡처.
- REQ-017 AC-3: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_409_surfaces_as_ledger_conflict_error` + `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` — 409 → `LedgerConflictError`; outbox row `status=conflict_blocked`, 후속 writer tick 에서 어댑터 재호출 없음.
- REQ-017 AC-4: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` — `trex_core/domain` AST 워크가 `httpx`/`fastapi`/`uvicorn`/`psycopg`/`redis`/`trex_core.adapters`/`trex_core.runtime`/`trex_core.application` 0 회 import 확인.
- REQ-018 AC-1: `tests/application/test_execution_ledger.py::test_buy_fill_enqueues_one_ledger_item_and_writer_records_it` — buy intent → execution.run_once → slot=holding, ledger_enqueued=1, writer.run_once → ledger.count()=1, `LedgerTrade(asset_id="PF-1", side="buy", qty=10, price=1000.0)`.
- REQ-018 AC-2: `tests/application/test_execution_ledger.py::test_sell_fill_enqueues_sell_ledger_item` — buy→holding→sell intent→execution.run_once → slot=closed, ledger 에 `["buy","sell"]` 순서.
- REQ-018 AC-3: 위 buy 테스트가 `t.execution_id.startswith("paper:PAPER-")`, `t.fee > 0.0`, `t.tax == 0.0` 직접 검증.
- REQ-018 AC-4: `tests/application/test_execution_ledger.py::test_portfolio_id_absent_skips_ledger_but_transitions_slot` — portfolio_id=None → ledger_enqueued=0, ledger_skipped=1, 슬롯은 HOLDING.
- REQ-018 AC-5: `tests/domain/test_domain_import_sweep.py` (도메인 격리) + `tests/application/test_execution_ledger.py` (오직 `ExecutionService._record_ledger` 만 outbox.enqueue 호출 — code review). `grep -r "ledger_outbox.enqueue\|LedgerOutbox.enqueue" trex_core/` 결과는 `ExecutionService` 단일 호출지점.
- REQ-019 AC-1: `tests/application/test_execution_ledger.py::test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 같은 trade 재 enqueue 시 outbox row 1개 유지, 별도 outbox 에서 재처리해도 ledger.count()=1.
- REQ-019 AC-2: `tests/application/test_execution_ledger.py::test_retryable_failure_then_eventually_completes` + `tests/application/test_ledger_outbox.py::test_writer_retries_until_success` — retryable 2회 + completed 1회로 ledger.count()=1, 슬롯은 즉시 HOLDING.
- REQ-019 AC-3: `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` + `tests/application/test_ledger_outbox.py::test_writer_marks_conflict_blocked_and_does_not_retry` — 후속 writer tick stats 모두 0.
- REQ-019 AC-4: `tests/application/test_execution_ledger.py::test_duplicate_completed_when_reprocessing_terminal_item` + `test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 별도 outbox 에 동일 trade 재 enqueue → writer.run_once stats.duplicates=1, stats.completed=0, ledger.count()=1.
- REQ-019 AC-5: `tests/application/test_execution_ledger.py::test_slot_transition_proceeds_when_ledger_is_down` — `InMemoryLedger(fail_n_then_succeed=100)` 으로 모든 writer tick 실패, slot 은 HOLDING, outbox row `status=PENDING` 유지(무유실).
- PG 통합 (DSN 제공 시): `tests/adapters/persistence/test_postgresql_ledger_outbox.py` 4건 — enqueue 멱등, writer completed/conflict_blocked/retryable 행 컬럼 검증. CI 환경에 DSN 없어 skip 4건은 명시적으로 PG 게이트.
### Manual
- baseline 문서 ↔ 코드 정합 리뷰 (REQ-020 AC-1/AC-2):
- `docs/runtime-flows.md` 새 섹션 `Flow: 체결 → ledger outbox` + `Flow: ledger writer` 가 `ExecutionService._record_ledger` (`trex_core/application/execution.py:117-141`) 과 `LedgerWriter.run_once`/`_process` (`trex_core/application/ledger_outbox.py:96-149`) 로직 정합. paper/KIS execution_id 합성 규칙은 `trex_core/adapters/broker/paper.py:73-79`, `trex_core/adapters/broker/kis.py:177-192` 와 일치.
- `docs/interface-contract.md` 새 섹션 `Interface: LedgerPort` + `Interface: LedgerOutbox/LedgerWriter` 가 `trex_core/application/ledger_port.py` 시그니처, `LedgerWriteResult.NEW/DUPLICATE_OK`, `LedgerConflictError`/`LedgerRetryableError` 와 정합. portfolio-hub endpoint 표는 `trex_core/adapters/ledger/portfolio_hub.py:_payload`/`record_trade` 와 정합.
- `docs/project-structure.md` 디렉토리 트리에 `trex_core/application/ledger_port.py`, `ledger_outbox.py`, `trex_core/adapters/ledger/`, persistence 의 `PostgreSQLLedgerOutbox` + `002_create_ledger_outbox.sql`, runtime 의 ledger writer 루프가 반영. 실제 디렉토리 트리(`find trex_core -type f -name '*.py'`)와 일치 확인.
- `docs/data-model.md` 새 엔티티 `LedgerTrade`(application DTO) + `ledger_outbox` 테이블 정의가 `002_create_ledger_outbox.sql` (PK `correlation_id`, status/last_outcome CHECK) 와 1:1 정합. `FillStatus` 확장(`execution_id/fee/tax`) 도 명시.
- code-search (REQ-020 AC-2 + REQ-018 AC-5 보조):
- `grep -r "LedgerOutbox.enqueue\|self._ledger_outbox.enqueue" trex_core/` → `trex_core/application/execution.py` 단 1곳(execution path 단일 기록 주체).
- `grep -r "import httpx\|from httpx" trex_core/domain/` → 결과 0(NFR-1 보강).
### Logs
- `.venv/bin/ruff check trex_core tests` → `All checks passed!` (style/lint blocker 0).
- 정적 import 스윕 자동화: `tests/domain/test_domain_import_sweep.py` 가 CI 의 일부로 회귀 가드.
- portfolio-hub 실 통합 evidence 는 본 batch 범위 밖(`PortfolioHubLedger` 는 `httpx.MockTransport` 로 contract 검증). 실 통합은 운영 환경에서 수집 — `Remaining Risks` 와 `docs/srs/Integration/req-017_…md` Notes 의 A-1/DATA 위임 항목으로 추적.
## Result
- Ready for Release: false
- Ready for Release: true
- Blocking Issues:
- 구현 및 검증 미진행.
- 없음.
- Notes:
- portfolio-hub 실 계약/자격증명 evidence 는 stub + contract 수준이며, 실 통합 검증은 운영 단계에서 수집한다(dcy-004 DATA 위임 사항).
- 부분체결 누적/정밀 세금 모델은 후속 Discovery 로 분리됨(이번 범위 밖).
+1 -1
View File
@@ -13,4 +13,4 @@
| BAT-003 | batch-lite | release-candidate | REQ-010 | dcy-002 | [bat-003](./bat-003_20260604_daemon-dotenv-config/) |
| BAT-004 | standard | release-candidate | REQ-011, REQ-012, REQ-013, REQ-014, REQ-015 | dcy-003 | [bat-004](./bat-004_20260604_broker-execution-and-outbox/) |
| BAT-005 | batch-lite | release-candidate | REQ-016 | dcy-003 | [bat-005](./bat-005_20260604_kis-token-redis-cache/) |
| BAT-006 | standard | draft | REQ-017, REQ-018, REQ-019, REQ-020 | dcy-004 | [bat-006](./bat-006_20260604_portfolio-hub-ledger-integration/) |
| BAT-006 | standard | release-candidate | REQ-017, REQ-018, REQ-019, REQ-020 | dcy-004 | [bat-006](./bat-006_20260604_portfolio-hub-ledger-integration/) |