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:
@@ -0,0 +1,246 @@
|
||||
# 0. 문서 상태
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-026_20260612_sell-transaction-tax-and-fee-policy
|
||||
- 이슈명: 매도 증권거래세 누락 + 수수료 정책 정정 (issue #24)
|
||||
- 작성 시각(KST): 2026-06-12
|
||||
- 마지막 갱신 시각(KST): 2026-06-12 (confirm-discovery: Justin Ko 승인 — 상태 confirmed 전환, §8 체크 완료, Freeze 플래그 갱신)
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: 없음
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 없음
|
||||
- 관련 TODO/메모: GitHub issue #24 (`bug` 라벨, 2026-06-11 trex-admin-daily-trading 4건 체결 증거 포함)
|
||||
- 해석:
|
||||
- 매도 증권거래세가 전 체결에서 `tax=0.0`으로 저장되어 실현 손익·일일 수익률이 과대 계산되는 운영 버그. 코드상 `dcy-004 OQ-3`로 미뤄둔 "정밀 세금 모델"이 운영에서 표면화된 사안.
|
||||
- 단순 핫픽스가 아니라 수수료율/세율 정책(매수·매도 구분, bp 단위, 세금 산식)을 계약으로 확정해야 하므로 신규 Discovery로 정식화한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 ledger 계열 Discovery(dcy-004 연동, dcy-015 outbox 백오프, dcy-018 cash write-back)는 fee/tax **정책 자체**를 다루지 않음. fee/tax 정확성은 미해결 영역.
|
||||
- `trex_core/application/broker_port.py:51` 주석이 "tax defaults to 0 (precise tax model is a follow-up Discovery, dcy-004 OQ-3)"로 후속 Discovery 필요를 명시 — 그 후속을 본 Discovery로 개설.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
|
||||
- 현재 상태
|
||||
- `trex_core/application/broker_port.py:118-119`: `_FEE_BPS_BUY = _FEE_BPS_SELL = 1.5`(0.015%) — 수수료만 계산. `calculate_fee(side, amount)` 단일 함수.
|
||||
- `trex_core/adapters/broker/paper.py:90`·`kis.py:213·230`: 체결 시 `tax=0.0` **하드코딩** — 매도 증권거래세가 전혀 반영되지 않음. 한국 주식 매도세(증권거래세+농특세 약 0.18~0.20%)는 수수료(0.015%)보다 10배 이상 큰 비용인데 누락.
|
||||
- 결과(issue #24 증거): 2026-06-11 4건 체결의 저장 `fee+tax` 합계 177.9원 vs 실제 기대 1,357원 — 차액 대부분이 누락된 매도세.
|
||||
- 기대 상태
|
||||
- 매도 체결 시 증권거래세를 정책 기준으로 계산해 `FillStatus.tax`에 반영하고 `ledger_outbox`에 정확히 저장한다.
|
||||
- 수수료율·세율을 단일 정책 지점(`calculate_fee`/`calculate_tax`)에서 관리하고, paper·KIS 어댑터가 동일 정책을 사용한다.
|
||||
- 이해관계자
|
||||
- 운영자/시스템 관리자 (Justin Ko): 정확한 일일 손익·수익률 리포트 필요
|
||||
- portfolio-hub(ledger 수신측): `fee_amount`/`tax_amount` 정확값 수신
|
||||
- 전략 모듈(trex-orb-v1): 비용 반영된 실현 손익 기반 의사결정
|
||||
- 제약사항
|
||||
- KIS 실 체결 응답에 수수료/세금 필드가 포함되면 우선 사용하고, 없으면 정책 추정값으로 폴백해야 함(브로커 정본 우선 원칙).
|
||||
- 정책 상수는 코드 하드코딩 대신 설정 가능성을 검토(세율 변경 대응).
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
|
||||
- 매도 round-trip 체결 후 운영자가 일일 리포트를 확인 → 수수료+세금이 실제 부과액과 일치하고 실현 손익이 과대 계산되지 않음.
|
||||
- KIS 라이브 매도 체결 시 브로커 응답의 세금/수수료가 있으면 그 값이, 없으면 정책 추정값이 ledger에 기록됨.
|
||||
- paper 브로커 검증 시에도 동일 정책으로 세금이 계산되어 라이브와 비교 가능한 ledger 엔트리 생성.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
|
||||
- 매도 증권거래세 산식 도입 — `calculate_tax(side, amount)`(매수 0, 매도 정책 세율) 추가 또는 `calculate_fee`를 fee+tax 동시 산출로 확장.
|
||||
- paper·KIS 어댑터의 `tax=0.0` 하드코딩 제거 — 정책 함수 호출로 교체, KIS는 브로커 응답값 우선·정책 폴백.
|
||||
- 수수료/세율 정책 상수를 env 설정으로 노출 (OQ-2 Closed) 및 baseline 문서(`runtime-flows.md`) fee/tax 흐름 갱신.
|
||||
|
||||
## 제비용 계산 공식 (OQ-1 Closed 기준)
|
||||
|
||||
정책 파라미터 (env 설정, 기본값):
|
||||
|
||||
```
|
||||
TREX_KIS_FEE_BPS = 1.5 # 수수료율 (매수·매도 공통, 1.5bps = 0.015%)
|
||||
TREX_KRX_TAX_SELL_BPS = 20.0 # KRX(KOSPI·KOSDAQ) 매도 증권거래세율 (20.0bps = 0.2%)
|
||||
```
|
||||
|
||||
> 변수명에 `KIS`/`KRX` 도메인 라벨이 붙지만, 이 값은 단일 정책 지점으로 **paper·KIS 어댑터가 동일하게 공유한다**(FR-2·NFR-2). paper 브로커는 KIS 제비용을 모사하므로 같은 상수를 사용한다.
|
||||
|
||||
체결 단위 제비용:
|
||||
|
||||
```
|
||||
amount = price × qty
|
||||
|
||||
fee = amount × TREX_KIS_FEE_BPS / 10_000 # 매수·매도 동일
|
||||
tax = amount × TREX_KRX_TAX_SELL_BPS / 10_000 # 매도만 적용; 매수 = 0
|
||||
total = fee + tax
|
||||
```
|
||||
|
||||
side별 전개:
|
||||
|
||||
| side | fee | tax | total |
|
||||
|------|-----|-----|-------|
|
||||
| buy | amount × 0.00015 | 0 | amount × 0.00015 |
|
||||
| sell | amount × 0.00015 | amount × 0.002 | amount × 0.00215 |
|
||||
|
||||
issue #24 역산 검증 (qty=1, OQ-1):
|
||||
|
||||
```
|
||||
BUY 291,500원 → fee=43.73원, tax=0원, total=43.73원
|
||||
SELL 301,000원 → fee=45.15원, tax=602원, total=647.15원
|
||||
BUY 295,000원 → fee=44.25원, tax=0원, total=44.25원
|
||||
SELL 298,500원 → fee=44.78원, tax=597원, total=641.78원
|
||||
─────────────────────────────────────────────────────
|
||||
합계 fee=177.9원, tax=1,199원, total=1,376.9원
|
||||
(이슈 기재 1,357원과 차이 약 20원 — 이슈 원작성자 추정 오차)
|
||||
```
|
||||
|
||||
## 영향 범위
|
||||
|
||||
- 코드 영향:
|
||||
- `trex_core/application/broker_port.py`: `calculate_fee` 정정 + `calculate_tax` 신규(또는 통합), 세율/수수료율 상수
|
||||
- `trex_core/adapters/broker/paper.py`: `tax=0.0` → `calculate_tax(...)` (line 90 주변)
|
||||
- `trex_core/adapters/broker/kis.py`: `tax=0.0` → 브로커 응답 파싱 + 정책 폴백 (line 213·230 주변)
|
||||
- `trex_core/application/ledger_outbox.py`·`execution.py`: fee/tax 전달 경로(계약 불변 가능성 높음, 검증 필요)
|
||||
- 문서 영향:
|
||||
- REQ-018(체결→원장 기록) change-req: fee/tax 산출 정책 명시
|
||||
- REQ-011(BrokerPort + paper double) change-req 가능성: 수수료/세율 정책 함수 계약
|
||||
- `docs/runtime-flows.md`: 체결→ledger fee/tax 산출 흐름
|
||||
- 운영 영향:
|
||||
- 배포 후 신규 체결부터 정확한 fee/tax 저장. **과거 잘못 저장된 행(2026-06-11 등) 보정 여부는 별도 결정**(OQ).
|
||||
- portfolio-hub 측 손익 재계산 영향 — 수신측 정합 확인 필요.
|
||||
|
||||
# 5. 요구사항 목록
|
||||
|
||||
## 기능 요구사항
|
||||
|
||||
- FR-1: 매도 체결에 대해 정책 기준 증권거래세를 계산해 `FillStatus.tax`에 반영한다. 매수 체결의 세금은 0으로 한다(한국 주식 매수 무세 전제).
|
||||
- FR-2: 수수료율·세율을 단일 정책 지점에서 관리하고 paper·KIS 어댑터가 동일 정책을 사용한다. `tax=0.0` 하드코딩을 제거한다.
|
||||
- FR-3: KIS 어댑터는 브로커 체결 응답에 수수료/세금 필드가 있으면 그 값을 정본으로 사용하고, 없을 때만 정책 추정값으로 폴백한다.
|
||||
- FR-4: `docs/runtime-flows.md`에 체결→ledger fee/tax 산출·저장 흐름을 갱신한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
|
||||
- NFR-1: fee/tax 계산은 결정적이며 단위 테스트로 매수/매도 각 케이스의 기대값을 검증 가능해야 한다(issue #24 증거 수치 회귀 테스트 포함).
|
||||
- NFR-2: paper·KIS 어댑터가 동일 입력(side, amount)에 동일 정책 결과를 산출한다(브로커 응답 미존재 시).
|
||||
- NFR-3: 정책 변경(세율 조정) 시 코드 한 지점 수정으로 전 경로에 반영된다.
|
||||
|
||||
## 범위 경계
|
||||
|
||||
- In Scope
|
||||
- 매도 증권거래세 산식 + 수수료율 정정
|
||||
- paper·KIS `tax=0.0` 하드코딩 제거 및 정책 함수 적용
|
||||
- KIS 브로커 응답 fee/tax 파싱(존재 시 정본)
|
||||
- REQ-018(+REQ-011) change-req + runtime-flows 갱신 + 회귀 테스트
|
||||
- Out of Scope
|
||||
- 과거 잘못 저장된 ledger 행 소급 보정: **Out of Scope** — 보정하지 않음 (OQ-3 Closed)
|
||||
- portfolio-hub 측 손익 재계산 로직 변경(수신측 저장소 책임)
|
||||
- 세율의 동적 설정 UI/운영 도구(필요 시 후속)
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
|
||||
## 리스크
|
||||
|
||||
- R-1 (High): 세율 산식 오류 시 손익이 반대 방향으로 다시 왜곡됨 — 검증 부족하면 버그를 버그로 교체.
|
||||
- 완화 방안: §4 공식 기준 기대 합계(fee 177.9 + tax 1,199 = **1,376.9원**)를 회귀 테스트 기준으로 고정. 매수/매도 각 케이스 단위 테스트. (이슈 기재 1,357원은 원작성자 추정 오차 약 20원 — baseline 아님.)
|
||||
- R-2 (Medium): KIS 실 체결 응답의 수수료/세금 필드명·단위(원 vs bp)를 잘못 파싱하면 라이브에서만 오류 발생(paper에서 미검출).
|
||||
- 완화 방안: KIS 응답 raw 필드 확인 후 파싱, 미확인 필드는 정책 폴백 + 로그. 라이브 스모크 시 실제 값 대조.
|
||||
- R-3 (Medium): 과거 저장 행 미보정 시 2026-06-11 이전 리포트는 계속 왜곡 상태로 남음.
|
||||
- 완화 방안: OQ-3에서 소급 보정 범위·방법 결정. 보정 안 하면 release 문서에 알려진 한계로 명시.
|
||||
|
||||
## 가정
|
||||
|
||||
- A-1: 한국 주식 기준 매수는 무세, 매도에 증권거래세(+농특세)가 부과된다. 정확한 세율 값은 사용자 확인 대상(OQ-1).
|
||||
- A-2: 신규 REQ 없이 REQ-018(+필요 시 REQ-011) change-req로 닫힌다 — 계약 구조는 유지, 산식·값만 정정.
|
||||
- A-3: KIS 체결 응답에 수수료/세금이 포함될 수 있으나 항상 보장되지는 않아 정책 폴백이 필요하다.
|
||||
|
||||
## 오픈 질문
|
||||
|
||||
- OQ-1: 적용할 정확한 매도 세율(증권거래세+농특세 합산 %)과 수수료율(매수/매도 bp)은?
|
||||
- 상태: Closed
|
||||
- 결정: 매도 증권거래세 **0.2%** (매수 무세), 수수료율 **0.015% (1.5bps)** 매수·매도 동일. issue #24 4건 체결 역산(qty=1 확인, 계산 기대 합계 1,376.9원)으로 현행 코드값 0.015% 정합 확인. 이슈 기재 1,357원은 약 20원 추정 오차.
|
||||
- 종료 조건: 충족
|
||||
- OQ-2: 정책 상수를 코드 상수로 둘지, 설정(env/TOML)으로 노출할지?
|
||||
- 상태: Closed
|
||||
- 결정: **env 설정**으로 노출한다. 확정 변수명:
|
||||
- `TREX_KIS_FEE_BPS` — 한국투자증권 수수료율 (bps, 기본 `1.5` = 0.015%, 매수·매도 공통)
|
||||
- `TREX_KRX_TAX_SELL_BPS` — KRX(KOSPI·KOSDAQ) 매도 증권거래세율 (bps, 기본 `20.0` = 0.2%)
|
||||
- 기존 패턴(`TREX_WATCHDOG_TIMEOUT`, `TREX_PAPER_FILL_AFTER_POLLS` 등) 준용 — `TREX_{도메인}_{파라미터}` 형식
|
||||
- 종료 조건: 충족
|
||||
- OQ-3: 과거 잘못 저장된 ledger 행(2026-06-11 등) 소급 보정 여부와 방법은?
|
||||
- 상태: Closed
|
||||
- 결정: **소급 보정하지 않는다** — 과거 행은 그대로 둔다. 배포 후 신규 체결부터 정확한 fee/tax 저장. Out of Scope 확정.
|
||||
- 종료 조건: 충족
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
|
||||
- S-1: 매도 체결의 `tax`가 정책 세율로 계산되어 0이 아닌 정확값으로 `ledger_outbox`에 저장된다. issue #24의 4건 시나리오 재현 시 `fee+tax` 합계가 §4 공식 기대값(fee 177.9 + tax 1,199 = **1,376.9원**)에 정확히 수렴한다.
|
||||
- S-2: paper·KIS 어댑터가 동일 정책으로 fee/tax를 산출하며, KIS는 브로커 응답값이 있으면 그 값을 사용한다.
|
||||
- S-3: 기존 단위·통합 테스트 회귀 없음 + 신규 fee/tax 회귀 테스트 green, `ruff check` clean.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
|
||||
- S-1 측정 방식: 단위 테스트 — issue #24 4건 체결값(price/qty) 입력 후 `fee+tax` 합계가 §4 공식 기대 1,376.9원에 수렴 확인 / 데이터 출처: issue #24 Evidence + §4 공식 + `tests/application/` 신규 테스트
|
||||
- S-2 측정 방식: paper·KIS 어댑터 단위 테스트로 동일 입력 동일 출력 확인 + KIS 응답값 우선 경로 테스트 / 데이터 출처: `tests/adapters/broker/`
|
||||
- S-3 측정 방식: `pytest` 전체 회귀 + `ruff check trex_core tests` / 데이터 출처: 로컬/CI 결과
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: 소급 보정(OQ-3) In/Out 확정 + 정책 상수 위치(OQ-2) 확정
|
||||
- 결과: OQ-2 env 설정 확정, OQ-3 소급 보정 Out of Scope 확정
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: §4 공식 기대값 1,376.9원 역산에 필요한 세율/수수료율(OQ-1) 확정
|
||||
- 결과: OQ-1 매도세 0.2%, 수수료 0.015% 확정 — §4 역산 합계 1,376.9원
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1(세율 오류) 회귀 테스트 기준값 확정 담당 Justin Ko
|
||||
- 결과: R-1 완화 방안 확정 — §4 기대 1,376.9원 회귀 테스트 기준 고정, Justin Ko 담당
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- 결과: OQ-1·OQ-2·OQ-3 모두 Closed — Open 항목 0개
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Justin Ko 승인 후 `confirm-discovery` 수행
|
||||
- 결과: Justin Ko 승인, confirm-discovery 완료
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md
|
||||
- 참조 문서:
|
||||
- GitHub issue #24 — fee/tax 저장 오류 증거(2026-06-11 4건 체결, 기대 1,357원 vs 저장 177.9원)
|
||||
- trex_core/application/broker_port.py:51·118-131 — fee 모델 + tax follow-up 주석(dcy-004 OQ-3)
|
||||
- trex_core/adapters/broker/paper.py:90, kis.py:213·230 — `tax=0.0` 하드코딩(변경 대상)
|
||||
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md — OQ-3 정밀 세금 모델 선행 참조
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
|
||||
## DECIDE
|
||||
|
||||
- 매도 세율: **0.2%**, 수수료율: **0.015% (1.5bps)** — 확정 (OQ-1 Closed)
|
||||
- 정책 상수 위치: **env 설정** — `TREX_KIS_FEE_BPS` (기본 1.5), `TREX_KRX_TAX_SELL_BPS` (기본 20.0) — 확정 (OQ-2 Closed)
|
||||
- 과거 ledger 행 소급 보정: **Out of Scope** — 확정 (OQ-3 Closed)
|
||||
|
||||
## CONFIRM
|
||||
|
||||
- KIS 체결 응답에 수수료/세금 필드가 실제 포함되는지 확인 — 포함 시 정본 우선, 미포함 시 정책 폴백(FR-3·R-2 연결). delivery 단계 KIS raw 응답 확인으로 종료.
|
||||
|
||||
## DATA
|
||||
|
||||
- 없음 — issue #24 4건 체결의 기대 수수료/세금 분해값은 §4 "제비용 계산 공식"에서 체결별로 제공됨(answered).
|
||||
|
||||
# 11. Discovery Freeze
|
||||
|
||||
## 섹션 참조
|
||||
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
|
||||
- Handoff Decision: REQ Drafting 진행 가능
|
||||
- Handoff Rationale: OQ-1(세율/수수료율), OQ-2(정책 위치), OQ-3(소급 보정) 3건 모두 Closed. §8 체크 완료. 구현 부재는 정상(신규 작업 정의 Discovery).
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin Ko
|
||||
- Confirmed At (KST): 2026-06-12
|
||||
@@ -31,3 +31,4 @@
|
||||
| dcy-023_20260610_allocated-capital-dead-field-removal | 20260610 | allocated_capital 죽은 필드 제거 — `enter --capital` 옵션 + slot/intent/snapshot 계약 정리 (issue #22) | [Discovery](./dcy-023_20260610_allocated-capital-dead-field-removal.md) | confirmed |
|
||||
| dcy-024_20260611_safety-sell-disable-mode | 20260611 | safety-sell 자율 청산 비활성화 모드 — per-instance TOML `safety_sell_enabled=false` 시 워커 미기동 (issue #15) | [Discovery](./dcy-024_20260611_safety-sell-disable-mode.md) | confirmed |
|
||||
| dcy-025_20260611_strategy-read-surface-slots-req | 20260611 | 전략 read 표면 재설계 — `trex.snapshot.req`→`trex.slots.req` + 슬롯 상태 필터 + 슬롯 단일 행(assets[]/available_slots/watchdog[] 흡수·제거) | [Discovery](./dcy-025_20260611_strategy-read-surface-slots-req.md) | confirmed |
|
||||
| dcy-026_20260612_sell-transaction-tax-and-fee-policy | 20260612 | 매도 증권거래세 누락 + 수수료 정책 정정 — `tax=0.0` 하드코딩 제거 + 단일 정책 지점 (issue #24) | [Discovery](./dcy-026_20260612_sell-transaction-tax-and-fee-policy.md) | confirmed |
|
||||
|
||||
Reference in New Issue
Block a user