feat(broker): BAT-038 — 매도 증권거래세 누락 정정 + fee/tax 단일 정책 지점 (dcy-026/REQ-018, issue #24)

dcy-026 confirm-discovery + REQ-018 change-req(CHG-20260612-01)→구현→
confirm-req-implemented(CHG-20260612-02) 전체 delivery.

- broker_port.py: calculate_tax(side, amount) 신규 + fee/tax 율 env 상수화
  (TREX_KIS_FEE_BPS=1.5, TREX_KRX_TAX_SELL_BPS=20.0). 매도 0.2%/매수 0.
- paper.py / kis.py: tax=0.0 하드코딩 3곳 제거 → 정책 함수 호출.
  KIS daily-ccld 응답에 fee/tax 필드 부재 → 정책 폴백(향후 정본 우선 훅).
- runtime-flows.md: 체결→ledger fee/tax 산출 흐름 갱신.
- tests: fee/tax 정책 단위 + issue #24 4건 회귀(fee+tax=1,376.9원).
  429 passed (+6), ruff clean.

REQ-018 Approved→Implemented, BAT-038 release-candidate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 17:57:15 +09:00
co-authored by Claude Opus 4.8
parent 08fdf680bf
commit 1cf13b060f
15 changed files with 695 additions and 24 deletions
@@ -10,26 +10,33 @@
## Requirement
- 집행 루프가 매수 체결(`pending_buy→holding`)·매도 체결(`pending_sell→closed`) 시 LedgerPort 로 거래를 기록한다. ledger path 의 `asset_id` 는 슬롯 `portfolio_id`(= asset_id)를 직접 사용하고 side/qty/price/execution_id/fee/tax/executed_at 을 함께 보낸다.
- `FillStatus`(broker_port) 가 `execution_id` 와 `fee` 를 운반하도록 계약을 확장한다. `execution_id` 는 결정적으로 합성한다(KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`, paper: `paper:{broker_order_id}:{filled_qty}`). `fee` 는 BrokerPort fee 계산(orbit `calculate_fee(side, amount)` 패턴), `tax` 는 기본 0 으로 한다.
- `FillStatus`(broker_port) 가 `execution_id`, `fee`, `tax` 를 운반하도록 계약을 확장한다. `execution_id` 는 결정적으로 합성한다(KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`, paper: `paper:{broker_order_id}:{filled_qty}`). `fee` 는 `calculate_fee(side, amount)`(매수·매도 공통 수수료율), `tax` 는 `calculate_tax(side, amount)`(매도 정책 증권거래세율, 매수 0)로 계산한다. 수수료율·세율은 단일 정책 지점에서 env(`TREX_KIS_FEE_BPS`=1.5, `TREX_KRX_TAX_SELL_BPS`=20.0)로 관리하고 paper·KIS 어댑터가 동일 정책을 사용한다(`tax=0.0` 하드코딩 금지). KIS 어댑터는 브로커 체결 응답에 수수료/세금 필드가 있으면 그 값을 정본으로 사용하고 없을 때만 정책 폴백한다. (dcy-026)
- 원장 기록 주체는 코어 집행 경로 단일로 한정한다(전략/도메인은 원장에 접근하지 않는다).
## Acceptance Criteria
1. Given idle 슬롯에 매수 intent → 체결, When 집행 루프 fill 전이, Then 슬롯이 holding 으로 전이되고 원장에 buy 거래 1건이 기록된다(asset_id = 슬롯 portfolio_id).
2. Given holding 슬롯에 매도 intent → 체결, When 집행 루프 fill 전이, Then 슬롯이 closed 로 전이되고 원장에 sell 거래 1건이 기록된다.
3. Given 체결된 주문, When fill 을 읽음, Then `FillStatus` 가 결정적 `execution_id` 와 계산된 `fee`(tax=0)를 운반한다.
3. Given 체결된 주문, When fill 을 읽음, Then `FillStatus` 가 결정적 `execution_id`, 계산된 `fee`, 계산된 `tax`(매도 정책 세율, 매수 0)를 운반한다.
4. Given 슬롯에 `portfolio_id` 가 없는 체결, When fill 전이, Then 원장 기록을 건너뛰거나(또는 정의된 정책으로 처리) 슬롯 전이 자체는 정상 수행된다.
5. Given 전략/도메인 코드, When 점검, Then 원장 기록은 코어 집행 경로에서만 발생한다(단일 기록 주체). (NFR-3)
6. Given 매도 체결(amount=price×qty), When fill 을 읽음, Then `tax` = `amount × TREX_KRX_TAX_SELL_BPS / 10_000`(기본 0.2%)로 계산되고, 매수 체결의 `tax` 는 0 이다.
7. Given 수수료/세율 정책, When paper·KIS 어댑터로 동일 `(side, amount)` 입력, Then 동일 정책 결과를 산출한다(단일 정책 지점 `calculate_fee`/`calculate_tax`, env 설정). `tax=0.0` 하드코딩이 코드에 남지 않는다.
8. Given KIS 체결 응답에 수수료/세금 필드 존재, When fill 파싱, Then 그 값을 정본으로 사용하고, 필드 부재 시에만 정책 추정값으로 폴백한다.
9. Given issue #24 4건 체결(BUY 291,500 / SELL 301,000 / BUY 295,000 / SELL 298,500, qty=1), When fee/tax 계산, Then `fee+tax` 합계가 1,376.9원(fee 177.9 + tax 1,199)에 수렴한다.
## Impacted Area
- Docs: docs/runtime-flows.md(체결→원장 기록 흐름), docs/interface-contract.md(FillStatus 확장) — REQ-020 에서 정합
- Modules: `trex_core/application/execution.py`(fill 전이에 ledger 기록 단계), `trex_core/application/broker_port.py`(FillStatus.execution_id/fee), `trex_core/adapters/broker/{paper,kis}.py`(execution_id 합성 + fee 계산)
- Tests: stub 원장으로 매수/매도 체결 시 buy/sell 1건 + 슬롯 전이, FillStatus execution_id/fee 운반, portfolio_id 부재 처리
- Docs: docs/runtime-flows.md(체결→원장 기록 + fee/tax 산출 흐름), docs/interface-contract.md(FillStatus 확장) — REQ-020 에서 정합
- Modules: `trex_core/application/execution.py`(fill 전이에 ledger 기록 단계), `trex_core/application/broker_port.py`(FillStatus.execution_id/fee/tax + `calculate_fee`/`calculate_tax` 단일 정책 지점 + env 세율 상수), `trex_core/adapters/broker/{paper,kis}.py`(execution_id 합성 + fee/tax 계산, KIS 응답 정본 우선)
- Tests: stub 원장으로 매수/매도 체결 시 buy/sell 1건 + 슬롯 전이, FillStatus execution_id/fee/tax 운반, portfolio_id 부재 처리, 매도 세율 + issue #24 4건 회귀(1,376.9원), paper·KIS 동일 정책 + KIS 응답 우선
## Notes
- 출처: dcy-004 FR-2, OQ-1(asset_id == portfolio_id, orbit 근거), OQ-3(execution_id/fee/tax 출처), NFR-3.
- OQ-1: 슬롯은 이미 buy intent 가 `portfolio_id` 를 공급해 캐시하므로 추가 매핑/조회 없이 path param 으로 사용한다.
- `portfolio_id` 부재 시 기록 skip vs 거부 정책은 구현 시 확정(AC-4 는 둘 다 허용하되 슬롯 전이 일관성을 요구).
- 멱등/무유실/재시도(correlation_id, outbox)는 REQ-019 에서 다룬다. 본 REQ 는 기록 호출 경로와 데이터 운반에 한정한다.
- (dcy-026/CHG-20260612-01) 매도 증권거래세 정책 확정: 매도세 0.2%(`TREX_KRX_TAX_SELL_BPS`=20.0), 수수료 0.015%(`TREX_KIS_FEE_BPS`=1.5, 매수·매도 공통). 변수명에 `KIS`/`KRX` 도메인 라벨이 있으나 단일 정책 지점으로 paper·KIS 가 동일 공유. dcy-004 OQ-3 "정밀 세금 모델" 후속 해소.
- 과거 잘못 저장된 ledger 행 소급 보정은 Out of Scope(dcy-026 OQ-3). 배포 후 신규 체결부터 정확값 저장.
- REQ-011(BrokerPort 추상화)는 본 변경 대상 아님: fee/tax 는 REQ-018 의 `FillStatus` 확장 + `calculate_fee`/`calculate_tax` 정책 영역이며, REQ-011 의 포트 계약(submit/poll/cancel async 더블 + 도메인 순수성)은 영향받지 않음.
## Change Log
@@ -267,3 +274,99 @@
#### Validation Plan
- BAT-019: delivery_failed WARN 로그(caplog) + 카운터 단위.
### CHG-20260612-01
- Date: 2026-06-12
- Author: Justin Ko
#### Change Summary
- (change-req, dcy-026/issue #24) 매도 증권거래세 누락 정정. `FillStatus.tax` 가 매도 체결에 대해 정책 세율(증권거래세)로 계산되도록 계약을 정정한다. 기존 `tax 는 기본 0 으로 한다` 규정과 `tax=0.0` 하드코딩(paper/kis 어댑터)을 제거하고, 수수료율·세율을 단일 정책 지점(`calculate_fee`/`calculate_tax`)에서 env 설정으로 관리한다. KIS 어댑터는 브로커 응답 fee/tax 가 있으면 정본 우선, 없으면 정책 폴백. 기존 `tax=0` 전제 구현/검증 evidence 무효화 → **Implemented → Approved 되돌림**.
#### Intent
- 한국 주식 매도 증권거래세(약 0.2%)가 전 체결에서 `tax=0.0` 으로 저장되어 실현 손익·일일 수익률이 과대 계산되던 운영 버그(issue #24, 2026-06-11 4건 체결 증거)를 닫는다. `broker_port.py` 가 dcy-004 OQ-3 로 미뤄둔 "정밀 세금 모델"을 정식 정책으로 확정한다.
#### Acceptance Criteria Delta
- Updated:
- AC-3: `FillStatus` 가 결정적 `execution_id` 와 계산된 `fee`(tax=0) 운반 → `fee` 와 **매도 정책 세율로 계산된 `tax`**(매수 tax=0) 운반.
- Added:
- AC-6: 매도 체결의 `tax` 는 `amount × TREX_KRX_TAX_SELL_BPS / 10_000`(기본 20.0bps = 0.2%)로 계산되고, 매수 체결의 `tax` 는 0 이다.
- AC-7: 수수료율·세율은 단일 정책 지점(`calculate_fee`/`calculate_tax`)에서 env(`TREX_KIS_FEE_BPS`=1.5, `TREX_KRX_TAX_SELL_BPS`=20.0)로 관리되고 paper·KIS 어댑터가 동일 정책을 사용한다. `tax=0.0` 하드코딩은 제거된다.
- AC-8: KIS 어댑터는 브로커 체결 응답에 수수료/세금 필드가 있으면 그 값을 정본으로 사용하고, 없을 때만 정책 추정값으로 폴백한다.
- AC-9: issue #24 4건 체결(BUY 291,500 / SELL 301,000 / BUY 295,000 / SELL 298,500, qty=1) 재현 시 `fee+tax` 합계가 §4 공식 기대값 **1,376.9원**(fee 177.9 + tax 1,199)에 수렴한다(회귀 테스트).
#### Impacted Area Delta
- Docs:
- docs/runtime-flows.md(체결→ledger fee/tax 산출·저장 흐름 갱신)
- Modules:
- `trex_core/application/broker_port.py`(`calculate_tax` 신규 + 세율 상수, env 로딩)
- `trex_core/adapters/broker/paper.py`(`tax=0.0` → `calculate_tax(...)`)
- `trex_core/adapters/broker/kis.py`(`tax=0.0` → 브로커 응답 파싱 + 정책 폴백)
- Tests:
- 매수/매도 fee·tax 단위 테스트 + issue #24 4건 회귀(1,376.9원) + KIS 응답 우선 경로 + paper·KIS 동일 정책 일치
#### Delivery Trace Delta
- Batches:
- BAT-038 (예정, dcy-026 fee/tax)
- Releases:
- none
#### Revalidation Impact
- Reverification Needed: yes (BAT-038 에서 매도 tax 계산·회귀 재검증)
- Existing Implementation Invalidated: yes (`tax=0` 전제 — AC-3 의 tax=0 evidence 및 paper/kis `tax=0.0` 하드코딩 무효)
- Status Recommendation: Implemented → **Approved** (구현/검증 후 confirm-req-implemented 로 복귀)
- Follow-up Action: confirm-req → draft-batch(BAT-038) → 구현·검증 → confirm-req-implemented
#### Validation Plan
- BAT-038: 매수/매도 fee·tax 단위 테스트(매도 0.2% 세율, 매수 tax=0), issue #24 4건 회귀(`fee+tax`=1,376.9원), KIS 응답 fee/tax 우선·폴백 경로, paper·KIS 동일 입력 동일 출력. `pytest` 전체 회귀 + `ruff check` clean.
### CHG-20260612-02
- Date: 2026-06-12
- Author: Justin Ko
#### Change Summary
- Status 변경: Approved → **Implemented** (confirm-req-implemented, 근거 batch BAT-038 release-candidate).
#### Intent
- BAT-038 verification.md 의 acceptance mapping + evidence(429 passed / fee·tax 정책 단위 / issue #24 1,376.9원 회귀 / `tax=0.0` 하드코딩 제거 grep / runtime-flows 동기화)로 AC-1~9 충족 → 구현 완료 상태로 동기화.
#### Acceptance Criteria Delta
- Added/Updated/Removed: 없음 (상태 전이만)
#### Impacted Area Delta
- Docs/Modules/Tests: 없음 (상태 전이만)
#### Delivery Trace Delta
- Batches:
- BAT-038 (release-candidate)
- Releases:
- none (release 편성은 별도 진행)
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: Implemented
- Follow-up Action: draft-release 로 release 편성(BAT-038 포함)
#### Validation Plan
- BAT-038 verification evidence 재참조로 충분.
- AC-3/6: `test_paper_sell_fill_carries_policy_tax`, `test_calculate_tax_sell_only`, `test_paper_buy_fill_has_zero_tax`.
- AC-7: `test_calculate_fee_same_rate_buy_and_sell`, `test_fee_and_tax_zero_for_nonpositive_amount`, `tax=0.0` grep 0건.
- AC-8: design Key Decision 3(KIS daily-ccld fee/tax 필드 부재 → 정책 폴백) + 코드 리뷰.
- AC-9: `test_issue_24_four_fill_regression` (1,376.9원).
- AC-1/2/4/5: BAT-006 evidence 유효 + 전체 회귀 429 passed.
+2 -2
View File
@@ -159,8 +159,8 @@
## Recent Change Log Summary
- 2026-06-12 :: REQ-018 Approved → **Implemented** (confirm-req-implemented, BAT-038): 매도 증권거래세 정책 계산 + `tax=0.0` 제거 구현·검증 완료. 429 passed (fee·tax 정책 6 신규 포함), issue #24 1,376.9원 회귀 green, `tax=0.0` 하드코딩 grep 0건, runtime-flows 동기화. Blocking Issues 없음.
- 2026-06-12 :: REQ-018 change-req (CHG-20260612-01, dcy-026/issue #24): 매도 증권거래세 누락 정정 — `FillStatus.tax` 매도 정책 세율(0.2%) 계산, `tax=0.0` 하드코딩(paper/kis) 제거, 단일 정책 지점 + env(`TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`). AC-3 갱신·AC-6~9 신규. `tax=0` evidence 무효 → Implemented → **Approved** 되돌림. BAT-038 구현·검증 후 복귀.
- 2026-06-12 :: REQ-073 Approved → **Implemented** (confirm-req-implemented, BAT-037): `available_slots: int|null` capacity hint 추가. AC-3 반전·AC-6 신규. 16 tests passed (AC-6 3개 포함), 423 전체 회귀 없음. Blocking Issues 없음.
- 2026-06-12 :: REQ-073 change-req (CHG-20260612-01): 응답 payload 에 `available_slots: int|null` 추가. AC-3 반전(available_slots 포함), AC-6 신규(hint 값 규칙). BAT-035 AC-3 evidence 무효 → Implemented → **Approved** 되돌림. 차기 batch 구현·검증 후 복귀.
- 2026-06-11 :: REQ-058 change-req (CHG-20260611-01): `reconcile_universe` 슬롯 생성 시 `symbol` 누락 수정. 시그니처 `set[str]` → `dict[str, str]`(portfolio_id→symbol), INSERT에 symbol 포함, `symbol IS NULL` 기존 슬롯 backfill 추가. AC-5 신규 추가. 기존 AC 1~4 evidence 유효 → **Implemented 유지**.
- 2026-06-11 :: BAT-035/036 완료 — REQ-073/074/075/021/034/063/048/005 Approved → **Implemented**, REQ-028 → **Deprecated** (trex.snapshot.req 코드 삭제·trex.slots.req 대체)
- 2026-06-11 :: REQ-028 Implemented → **Deprecated** (BAT-035, dcy-025): `trex.snapshot.req` envelope (`assets[]`/`View`/`build_snapshot_envelope`) 코드 삭제 완료. 후속 표면은 REQ-073/074 (`trex.slots.req`) 로 대체.