# BAT-038 Design - Status: confirmed - Batch: BAT-038 ## Architecture Summary - `broker_port.py` 의 fee 단일 정책 지점을 fee+tax 로 확장한다. 기존 `calculate_fee(side, amount)` 옆에 `calculate_tax(side, amount)` 를 추가하고, 수수료율·세율을 env 로딩 상수로 통일한다(`_FEE_BPS_*` 하드코딩 → env 기본값). - paper/kis 어댑터의 3개 `tax=0.0` 사이트(paper.py:90, kis.py:213·230)를 `calculate_tax(side, amount)` 호출로 교체한다. - KIS daily-ccld 응답은 fee/tax 필드를 제공하지 않으므로(아래 Key Decision 3) KIS 도 paper 와 동일하게 정책 추정값을 사용한다. 브로커 응답 정본 우선(FR-3)은 향후 확장 가능한 guarded 파싱 훅으로 표현하되 현재 endpoint 에서는 폴백 경로만 활성. - fee/tax 전달 경로(`execution.py:_record_ledger` → `LedgerTrade`)는 이미 `fs.tax` 를 운반하므로 계약 변경 없음(planning R-3 점검 완료). ## Changed Areas - Modules: - `trex_core/application/broker_port.py`: - `_FEE_BPS_BUY`/`_FEE_BPS_SELL` → env(`TREX_KIS_FEE_BPS`, 기본 1.5) 로딩 상수로 통일 - `_TAX_BPS_SELL` 신규 → env(`TREX_KRX_TAX_SELL_BPS`, 기본 20.0) - `calculate_tax(side, amount) -> float` 신규: 매도 `amount × _TAX_BPS_SELL / 10_000`, 매수 0 - `FillStatus.tax` 주석 갱신(0 고정 → 매도 정책 계산, dcy-026) - `trex_core/adapters/broker/paper.py`: line 90 `tax=0.0` → `tax=calculate_tax(o.side, o.filled * o.price)` - `trex_core/adapters/broker/kis.py`: - line 213(전량)·230(부분) `tax=0.0` → `calculate_tax(side, ccld_qty * fill_price)` - `calculate_tax` import 추가 - (선택) `_parse_broker_cost(row)` guarded 훅 — fee/tax 필드 부재 시 None → 정책 폴백(FR-3 future-proof) - Tests: - `tests/application/` 또는 `tests/adapters/broker/`: 매수/매도 fee·tax 단위, issue #24 4건 회귀(1,376.9원), paper·KIS 동일 정책, KIS 폴백 경로 - Docs: `docs/runtime-flows.md` line 254 ## Key Decisions 1. **`calculate_tax` 신규 함수**(통합 아님): `calculate_fee` 와 대칭으로 별도 함수. side 분기 단순(매수 0/매도 세율), 호출부에서 `fee=calculate_fee(...)`, `tax=calculate_tax(...)` 명시적 페어. 단일 정책 지점 원칙 충족(AC-7). 2. **env 로딩, 기본값 1.5 / 20.0**: 기존 `TREX_*` os.environ 패턴(config.py 사례) 준용. 모듈 로드 시 `float(os.environ.get("TREX_KIS_FEE_BPS", "1.5"))`, `float(os.environ.get("TREX_KRX_TAX_SELL_BPS", "20.0"))`. paper·KIS 가 동일 모듈 상수 공유 → 동일 정책 보장(NFR-2). 3. **KIS 응답 fee/tax 필드 부재 → 항상 정책 폴백** (R-2 해소): 어댑터가 쓰는 `주식일별주문체결조회`(inquire-daily-ccld, `output1`)는 per-fill 수수료/증권거래세 필드를 제공하지 않는다(`tot_ccld_amt` 만 존재, docs/broker/kis/rest/국내주식/주문/계좌/021 확인). 따라서 KIS 도 `calculate_fee`/`calculate_tax` 정책값을 사용한다. FR-3 "브로커 정본 우선" 은 필드가 없어 구조적으로 폴백 경로만 활성 — **오파싱 리스크 자체가 제거됨**. 향후 fee/tax 노출 endpoint 도입 시 guarded 훅 지점에 파싱 추가. 4. **소급 보정 없음**(dcy-026 OQ-3): 과거 행 미변경, 신규 체결부터 정확값. ## Edge Cases - `amount <= 0`: `calculate_tax` 도 `calculate_fee` 처럼 0 반환(0/음수 가드). - 매수 체결: `calculate_tax("buy", amount) == 0` (AC-6 매수 무세). - 부분 체결(kis.py:230): 체결분 `ccld_qty * fill_price` 기준으로 fee/tax 계산(전량과 동일 공식, 체결분만). - `fill_price == 0`(avg 부재): amount=0 → fee/tax 0(기존 fee 동작과 일관). - KIS 응답 fee/tax 필드 부재(현재 항상): 정책 폴백, 로그 불필요(설계상 기본 경로). ## Architecture Impact - runtime-flows ## Reference Doc Update Plan - project-structure.md: none (모듈 구조 불변, 함수 1개 추가만) - runtime-flows.md: line 254 `체결 → ledger outbox` flow — "계산된 `fee`(...), `tax=0` 을 운반" → "계산된 `fee`(`calculate_fee`)·`tax`(`calculate_tax`, 매도 정책 세율 0.2%/매수 0) 를 운반" 으로 갱신. fee/tax 단일 정책 지점(env `TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`) 주석 추가.