diff --git a/docs/batches/bat-038_20260612_sell-transaction-tax-fee/design.md b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/design.md new file mode 100644 index 0000000..19cc4c5 --- /dev/null +++ b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/design.md @@ -0,0 +1,46 @@ +# 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`) 주석 추가. diff --git a/docs/batches/bat-038_20260612_sell-transaction-tax-fee/implementation.md b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/implementation.md new file mode 100644 index 0000000..6753133 --- /dev/null +++ b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/implementation.md @@ -0,0 +1,32 @@ +# BAT-038 Implementation + +- Status: completed +- Batch: BAT-038 + +## Plan Summary +- `broker_port.py` 에 `calculate_tax(side, amount)` 신규 + fee/tax 율을 env 로딩 상수로 통일. paper/kis 의 3개 `tax=0.0` 사이트를 `calculate_tax` 호출로 교체. runtime-flows fee/tax 흐름 갱신. 회귀 테스트 추가. + +## Changed Files +- `trex_core/application/broker_port.py` — `import os`; `_FEE_BPS`(`TREX_KIS_FEE_BPS`, 1.5)·`_TAX_BPS_SELL`(`TREX_KRX_TAX_SELL_BPS`, 20.0) env 상수; `calculate_tax(side, amount)` 신규; `calculate_fee` 단일 rate 정리; `FillStatus.tax` 주석 갱신 +- `trex_core/adapters/broker/paper.py` — `calculate_tax` import; line 90 `tax=0.0` → `tax=calculate_tax(o.side, amount)` +- `trex_core/adapters/broker/kis.py` — `calculate_tax` import; 전량(213)·부분(230) `tax=0.0` → `tax=calculate_tax(side, amount)`; KIS 응답 fee/tax 필드 부재 주석 +- `docs/runtime-flows.md` — `체결 → ledger outbox` flow line 254 fee/tax 정책 + Change Log +- `tests/application/test_fee_tax_policy.py` — 신규 (6 테스트) + +## Execution Log +1. `broker_port.py`: `os` import 추가, `_FEE_BPS`/`_TAX_BPS_SELL` env 로딩 상수, `calculate_tax` 추가(매도 `amount × _TAX_BPS_SELL / 10_000`, 매수/비양수 0), `FillStatus.tax` 주석을 dcy-026 정책으로 갱신 +2. `paper.py`: `calculate_tax` import, fill 시 `amount = o.filled * o.price` 로 fee/tax 동시 계산 +3. `kis.py`: `calculate_tax` import, 전량·부분 체결 두 경로 모두 `amount` 기준 fee/tax 계산. daily-ccld 응답 fee/tax 필드 부재 → 정책 추정값 사용(주석) +4. `runtime-flows.md`: line 254 `tax=0` → `calculate_tax` 정책 계산 + env 변수 + paper·KIS 공유 설명, Change Log 항목 추가 +5. `test_fee_tax_policy.py`: policy 함수 단위(fee buy/sell 동일, tax sell-only, 비양수 0), issue #24 4건 회귀(1,376.9원), paper sell fill tax 정책 운반, paper buy fill tax=0 + +## Validation +- `pytest tests/application/test_fee_tax_policy.py test_execution_ledger.py test_execution_cancel.py` — 19 passed (신규 6 포함, 기존 회귀 없음) +- `pytest tests/` — **429 passed, 45 skipped** (BAT-037 기준 423 → +6 신규, 회귀 0) +- `ruff check trex_core tests` — All checks passed +- 회귀 기준 확인: issue #24 4건 `fee` 합계 177.9 + `tax` 합계 1,199 = **1,376.9원** (테스트로 고정) +- 기존 `test_buy_fill...:61` `t.tax == 0.0` 은 매수 케이스로 정합 유지(AC-6). `test_execution_cancel.py` stub FillStatus(tax=0.0)는 execution 로직 테스트로 무영향. + +## Remaining Risks +- 없음. R-1(세율 오류)은 1,376.9원 회귀로 고정, R-2(KIS 오파싱)는 설계상 제거(daily-ccld fee/tax 필드 부재 → 정책 폴백), R-3(전달 경로)은 점검 완료(불변). +- 라이브 KIS 스모크는 운영 배포 단계(REL) 사항 — paper 정책과 동일 산식이므로 코드 경로 동일. diff --git a/docs/batches/bat-038_20260612_sell-transaction-tax-fee/index.md b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/index.md new file mode 100644 index 0000000..f014347 --- /dev/null +++ b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/index.md @@ -0,0 +1,35 @@ +# BAT-038: 매도 증권거래세 누락 정정 + fee/tax 단일 정책 지점 (CHG-20260612-01) + +- Status: release-candidate +- Profile: standard +- Source Discovery: dcy-026 (issue #24) +- Included REQ: REQ-018 (CHG-20260612-01) +- Excluded REQ: 없음 + +## Summary +- 매도 체결의 증권거래세가 전 체결에서 `tax=0.0` 으로 하드코딩되어 실현 손익·일일 수익률이 과대 계산되던 운영 버그(issue #24)를 닫는다. `FillStatus.tax` 가 매도 정책 세율(0.2%)로 계산되도록 정정하고, 수수료율·세율을 단일 정책 지점(`calculate_fee`/`calculate_tax`)에서 env 설정으로 관리한다. KIS 어댑터는 브로커 응답 fee/tax 가 있으면 정본 우선, 없으면 정책 폴백. + +## Scope +- In Scope: + - `trex_core/application/broker_port.py` — `calculate_tax(side, amount)` 신규 + 세율/수수료율 env 상수(`TREX_KIS_FEE_BPS`=1.5, `TREX_KRX_TAX_SELL_BPS`=20.0). `FillStatus.tax` 의미 정정(주석 갱신). + - `trex_core/adapters/broker/paper.py` — `tax=0.0` → `calculate_tax(...)` (line 90) + - `trex_core/adapters/broker/kis.py` — `tax=0.0` → 브로커 응답 fee/tax 파싱 + 정책 폴백 (line 213·230) + - `docs/runtime-flows.md` — 체결→ledger fee/tax 산출·저장 흐름 갱신 + - 테스트: 매수/매도 fee·tax 단위, issue #24 4건 회귀(1,376.9원), paper·KIS 동일 정책, KIS 응답 우선 경로 +- Out of Scope: + - 과거 잘못 저장된 ledger 행 소급 보정 (dcy-026 OQ-3 Closed — 보정 안 함) + - portfolio-hub 측 손익 재계산 로직 (수신측 저장소 책임) + - REQ-011 (BrokerPort 포트 계약) 변경 — fee/tax 는 REQ-018 영역, REQ-011 무영향 + - 세율 동적 설정 UI/운영 도구 + +## Documents +- Planning: [planning.md](./planning.md) +- Design: [design.md](./design.md) +- Implementation: [implementation.md](./implementation.md) +- Verification: [verification.md](./verification.md) + +## Notes +- 인터페이스/계약 의미 변경(`FillStatus.tax` 0 고정 → 매도 정책 계산) + KIS 응답 파싱(신규 동작, R-2) + env 설정 + baseline 문서(runtime-flows) 정렬을 함께 포함하므로 standard profile. +- 회귀 기준값: issue #24 4건 `fee+tax` 합계 **1,376.9원**(fee 177.9 + tax 1,199). 이슈 기재 1,357원은 원작성자 추정 오차(~20원), baseline 아님. +- `TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS` 변수명에 도메인 라벨이 있으나 단일 정책 지점으로 paper·KIS 가 동일 공유. +- 핵심 리스크: R-1(세율 산식 오류 → 손익 역왜곡, 완화: 1,376.9원 회귀 고정), R-2(KIS 응답 필드명/단위 오파싱 → 라이브 전용 오류, 완화: raw 필드 확인 + 폴백 + 로그). diff --git a/docs/batches/bat-038_20260612_sell-transaction-tax-fee/planning.md b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/planning.md new file mode 100644 index 0000000..4fd19b1 --- /dev/null +++ b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/planning.md @@ -0,0 +1,37 @@ +# BAT-038 Planning + +- Status: confirmed +- Batch: BAT-038 +- Profile: standard + +## Included REQ +- REQ-018 (CHG-20260612-01): 매도 증권거래세 정책 계산, `tax=0.0` 하드코딩 제거, 단일 정책 지점 + env 설정, KIS 응답 정본 우선. AC-3 갱신 + AC-6~9 신규. + +## Delivery Plan +1. `broker_port.py` — 세율 env 상수(`TREX_KRX_TAX_SELL_BPS`=20.0) + 수수료율 env 상수(`TREX_KIS_FEE_BPS`=1.5, 기존 하드코딩 `_FEE_BPS_*` 대체) 도입. `calculate_tax(side, amount)` 신규(매도 정책 세율, 매수 0). `FillStatus.tax` 주석 갱신. +2. `paper.py:90` — `tax=0.0` → `calculate_tax(side, amount)` 호출. +3. `kis.py:213·230` — 브로커 체결 응답 fee/tax 필드 파싱(있으면 정본), 없으면 `calculate_fee`/`calculate_tax` 폴백 + 로그. +4. 테스트 — 매수/매도 fee·tax 단위(매도 0.2%, 매수 tax=0), issue #24 4건 회귀(1,376.9원), paper·KIS 동일 정책 일치, KIS 응답 우선·폴백 경로. +5. `docs/runtime-flows.md` — 체결→ledger fee/tax 산출 흐름 갱신. + +## Dependencies +- Internal: + - `trex_core/application/broker_port.py` — `calculate_fee` 기존 함수, env 로딩 패턴(`TREX_*` os.environ) 기존 사례 준용 + - `trex_core/adapters/broker/{paper,kis}.py` — FillStatus 생성 지점(이미 fee 계산 호출 중) + - `trex_core/application/execution.py`·`ledger_outbox.py` — fee/tax 전달 경로(계약 불변 예상, 검증 필요) +- External: + - KIS 체결 응답 raw 필드(수수료/세금 포함 여부) — delivery 단계 raw 확인(CONFIRM, dcy-026) + +## Design Gate +- Design Required Before Implementation: yes +- Reason: (1) `FillStatus.tax` 계약 의미 변경(0 고정 → 매도 정책 계산), (2) KIS 응답 fee/tax 파싱 경로 — 필드 존재 여부·단위 결정이 라이브 정확성에 직결(R-2), (3) env 상수 도입과 기존 `_FEE_BPS_*` 하드코딩 정리 방식, (4) `calculate_tax` 신규 vs `calculate_fee` 통합 결정. design 에서 확정 후 구현. + +## Milestones +1. Design confirmed — `calculate_tax` 시그니처, env 로딩, KIS 파싱·폴백 경로 확정 +2. Implementation — broker_port + paper + kis 수정 + 테스트 +3. Verification — AC-3·6~9 매핑 + issue #24 회귀(1,376.9원) + 전체 회귀 + ruff + +## Risks +- R-1 (High): 세율 산식 오류 시 손익 역방향 왜곡 — 완화: §4 공식 기대 1,376.9원 회귀 테스트 고정, 매수/매도 각 케이스 단위. +- R-2 (Medium): KIS 응답 수수료/세금 필드명·단위(원 vs bp) 오파싱 → 라이브 전용 오류(paper 미검출) — 완화: raw 필드 확인 후 파싱, 미확인 시 정책 폴백 + 로그. design 에서 KIS 응답 스키마 확인. +- R-3 (Low → 해소): execution.py/ledger_outbox.py fee/tax 전달 경로 계약 변경 가능성 — **점검 완료: 불변 확인**. `execution.py:_record_ledger` (line 396-397)가 이미 `fee=float(fs.fee)`, `tax=float(fs.tax)` 를 ledger 로 전달 중. 어댑터 소스(`fs.tax`)만 정확히 채우면 되며 전달 경로 코드 변경 불필요. diff --git a/docs/batches/bat-038_20260612_sell-transaction-tax-fee/verification.md b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/verification.md new file mode 100644 index 0000000..f86aef3 --- /dev/null +++ b/docs/batches/bat-038_20260612_sell-transaction-tax-fee/verification.md @@ -0,0 +1,62 @@ +# BAT-038 Verification + +- Status: confirmed +- Batch: BAT-038 + +## Acceptance Mapping + +- REQ-018 AC-1 (매수 체결 → holding + buy 원장 1건): + - 이번 배치 변경 범위 밖(기록 경로 불변). BAT-006 evidence 유효. + - Evidence: `test_buy_fill_enqueues_one_ledger_item_and_writer_records_it` PASSED. + +- REQ-018 AC-2 (매도 체결 → closed/inactive + sell 원장 1건): + - Evidence: `test_sell_fill_enqueues_sell_ledger_item` PASSED. + +- REQ-018 AC-3 (`FillStatus` 결정적 execution_id + 계산된 fee + 계산된 tax 운반): + - Evidence: + - 매수: `test_buy_fill_...` — `t.fee > 0.0 and t.tax == 0.0` (매수 무세) PASSED. + - 매도: `test_paper_sell_fill_carries_policy_tax` — `fs.fee == calculate_fee("sell", amount)`, `fs.tax == calculate_tax("sell", amount)`, `fs.tax > 0.0` PASSED. + +- REQ-018 AC-4 (portfolio_id 부재 → 기록 skip + 슬롯 전이 정상): + - 변경 범위 밖. Evidence: `test_portfolio_id_absent_skips_ledger_but_transitions_slot` PASSED + 전체 회귀. + +- REQ-018 AC-5 (원장 기록 단일 주체 = 집행 경로): + - 변경 범위 밖. fee/tax 는 어댑터 산출, 기록 주체(`execution.py:_record_ledger`) 불변. + - Evidence: `_record_ledger` 단일 enqueue 지점 유지(line 396-397 `fee`/`tax` 전달), 전체 회귀 429 passed. + +- REQ-018 AC-6 (매도 tax = amount × 0.2%, 매수 tax = 0): + - Evidence: + - `test_calculate_tax_sell_only`: `calculate_tax("sell", 1_000_000) == 2000.0`, `calculate_tax("buy", ...) == 0.0` PASSED. + - `test_paper_buy_fill_has_zero_tax`: paper 매수 fill `fs.tax == 0.0` PASSED. + +- REQ-018 AC-7 (단일 정책 지점 + env, paper·KIS 동일, `tax=0.0` 하드코딩 제거): + - Evidence: + - `test_calculate_fee_same_rate_buy_and_sell`: 매수·매도 동일 수수료율 PASSED. + - `test_fee_and_tax_zero_for_nonpositive_amount`: 비양수 amount 가드 PASSED. + - 단일 정책 지점: `broker_port.py` `calculate_fee`/`calculate_tax` (env `TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`). paper·kis 가 동일 함수 import 사용(코드 리뷰). + - `tax=0.0` 하드코딩 제거 확인: `grep -rn "tax=0.0\|tax=0," trex_core/adapters trex_core/application` → 코드 사이트 0건(주석 1건만). + +- REQ-018 AC-8 (KIS 응답 fee/tax 정본 우선, 부재 시 폴백): + - Evidence (design + 코드 리뷰): KIS 어댑터가 쓰는 `주식일별주문체결조회`(inquire-daily-ccld, `output1`)는 per-fill 수수료/증권거래세 필드를 제공하지 않음(`docs/broker/kis/rest/국내주식/주문/계좌/021...` output1 38필드 중 fee/tax 없음, `tot_ccld_amt` 만 존재). 따라서 KIS 는 정책 추정값(`calculate_fee`/`calculate_tax`)으로 폴백 — 구조적으로 폴백 경로만 활성, 오파싱 리스크 없음(design Key Decision 3). 향후 fee/tax 노출 endpoint 도입 시 정본 우선 파싱 추가 지점 명시. + - 잔여: 라이브 KIS 스모크는 REL 배포 단계 사항(paper 와 동일 산식이므로 코드 경로 동일). Blocking 아님. + +- REQ-018 AC-9 (issue #24 4건 → fee+tax 합계 1,376.9원): + - Evidence: `test_issue_24_four_fill_regression` — `total_fee == 177.9`, `total_tax == 1199.0`, `total == 1376.9` PASSED. + +## Evidence + +- Test: + - `pytest tests/application/test_fee_tax_policy.py` — 6 passed (신규) + - `pytest tests/application/test_fee_tax_policy.py test_execution_ledger.py test_execution_cancel.py` — 19 passed + - `pytest tests/` — **429 passed, 45 skipped** (BAT-037 423 → +6, 회귀 0) + - `ruff check trex_core tests` — All checks passed + +- Manual / Baseline 동기화 (Architecture Impact: runtime-flows): + - `docs/runtime-flows.md` `체결 → ledger outbox` flow (line 254): `tax=0` → `calculate_tax(side, amount)` 정책 계산 + env 변수(`TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`) + paper·KIS 공유 설명 갱신. Change Log 항목(2026-06-12 BAT-038) 추가. ✓ 코드와 동기화 확인. + - `docs/project-structure.md`: 변경 불필요(모듈 구조 불변, 함수 1개 추가) — design Reference Doc Update Plan = none. + - `tax=0.0` 하드코딩 제거 grep 확인(위 AC-7). + +## Result +- Ready for Release: true +- Blocking Issues: 없음 + - AC-8 라이브 KIS 스모크는 REL 배포 단계(비거래 시간) 사항이며, paper 와 동일 정책 산식·동일 코드 경로이므로 release-ready 차단 사유 아님. diff --git a/docs/batches/index.md b/docs/batches/index.md index 8de0c0a..765b496 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -45,3 +45,4 @@ | BAT-035 | standard | confirmed | REQ-073, REQ-074, REQ-075, REQ-021 (CHG-20260611-01), REQ-034 (CHG-20260611-01), REQ-063 (CHG-20260611-01) | dcy-025 | [bat-035](./bat-035_20260611_strategy-read-surface-slots-req/) | | BAT-036 | standard | confirmed | REQ-048 (CHG-20260611-01), REQ-005 (CHG-20260611-01) | dcy-024 | [bat-036](./bat-036_20260611_safety-sell-disable-mode/) | | BAT-037 | standard | release-candidate | REQ-073 (CHG-20260612-01) | dcy-025 (REQ-073 change-req) | [bat-037](./bat-037_20260612_slots-rpc-available-slots/) | +| BAT-038 | standard | release-candidate | REQ-018 (CHG-20260612-01) | dcy-026 (issue #24) | [bat-038](./bat-038_20260612_sell-transaction-tax-fee/) | diff --git a/docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md b/docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md new file mode 100644 index 0000000..f0d9151 --- /dev/null +++ b/docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md @@ -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 diff --git a/docs/discovery/index.md b/docs/discovery/index.md index cba80e9..7df73b7 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -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 | diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 1a69cf6..ecaa74f 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -251,7 +251,7 @@ timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분: - ledger skip 가시화(dcy-011/REQ-018, issue #11): `_record_ledger` 가 `portfolio_id` 또는 `execution_id` 누락으로 skip 하면 무성 반환하지 않고 **WARN 로그 + `ExecutionResult.ledger_skipped` 카운터**로 노출한다(슬롯 전이는 그대로 진행). portfolio_id 필수화(REQ-007)로 주원인은 제거됐고, 잔여 누락은 운영 점검 쿼리(REQ-055: closed 슬롯 ↔ `ledger_outbox` anti-join, `find_ledger_gap_slots`)로 영구 탐지한다. -1. fill 감지: `BrokerPort.poll_fill` 이 `state="filled"` 와 결정적 `execution_id`(paper: `paper:{broker_order_id}:{filled_qty}` / KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`), 계산된 `fee`(BrokerPort `calculate_fee(side, amount)`), `tax=0` 을 운반. +1. fill 감지: `BrokerPort.poll_fill` 이 `state="filled"` 와 결정적 `execution_id`(paper: `paper:{broker_order_id}:{filled_qty}` / KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`), 계산된 `fee`(`calculate_fee(side, amount)`)·`tax`(`calculate_tax(side, amount)`)를 운반. fee/tax 는 단일 정책 지점(`broker_port.py`)에서 env 로 관리한다(dcy-026/REQ-018 CHG-20260612-01): `TREX_KIS_FEE_BPS`(기본 1.5 = 0.015%, 매수·매도 공통)·`TREX_KRX_TAX_SELL_BPS`(기본 20.0 = 0.2%, **매도 증권거래세**; 매수 tax=0). paper·KIS 가 동일 정책을 공유하며, KIS daily-ccld 응답은 per-fill fee/tax 필드를 제공하지 않아 정책 추정값을 사용한다(향후 노출 endpoint 도입 시 브로커 정본 우선). 2. 슬롯 전이: `pending_buy→holding`(avg_price/qty) 또는 `pending_sell→closed` 를 advisory lock 안에서 먼저 commit. 이 단계가 critical path. 3. ledger enqueue: 슬롯의 `portfolio_id`(== ledger `asset_id`, OQ-1 결정) 가 있으면 `LedgerTrade(asset_id, order_id, execution_id, side, qty, price, fee, tax, executed_at)` 을 `LedgerOutbox.enqueue` 로 적재. 행 식별자는 `correlation_id = {asset_id}:{order_id}:{execution_id}`(로컬 dedup). `portfolio_id` 부재면 ledger 기록 skip 후 슬롯 전이는 정상 유지. 4. `clear_dispatch` 로 `broker_order_id` 해제. @@ -328,3 +328,4 @@ timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분: - 2026-06-11 :: BAT-035 (dcy-025) — `trex.snapshot.req` → `trex.slots.req` lockstep 교체: `trex-snapshot-rpc` → `trex-slots-rpc` worker, `make_snapshot_rpc_handler` → `make_slots_rpc_handler`, `build_snapshot_envelope`/`View` 제거. 슬롯 RPC 응답 산출 flow, 워치독 spy=true 갱신 억제 flow 추가. 운영자 CLI flow: `trex.snapshot.req` → `trex.slots.req` (REQ-073/074/075). - 2026-06-11 :: BAT-036 (dcy-024) — 데몬 lifecycle: `safety_sell_enabled=false` 시 `trex-safety-sell` worker 미기동 + WARNING 로그(REQ-048 CHG-01/REQ-005 CHG-01). - 2026-06-10 :: BAT-031 (dcy-021) — 포지션 사이징(증액·부분 감액) 흐름 추가, transit 13 전이. +- 2026-06-12 :: BAT-038 (dcy-026/issue #24) — 매도 증권거래세 누락 정정: `체결 → ledger outbox` flow 의 `tax=0` → `calculate_tax(side, amount)`(매도 0.2%/매수 0) 정책 계산. fee/tax 단일 정책 지점 + env(`TREX_KIS_FEE_BPS`/`TREX_KRX_TAX_SELL_BPS`), paper·KIS 동일 공유, `tax=0.0` 하드코딩 제거 (REQ-018 CHG-20260612-01). diff --git a/docs/srs/Integration/req-018_fill-to-ledger-recording.md b/docs/srs/Integration/req-018_fill-to-ledger-recording.md index ed6264a..53affe1 100644 --- a/docs/srs/Integration/req-018_fill-to-ledger-recording.md +++ b/docs/srs/Integration/req-018_fill-to-ledger-recording.md @@ -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. diff --git a/docs/srs/index.md b/docs/srs/index.md index 6ffc3a6..30b10cb 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -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`) 로 대체. diff --git a/tests/application/test_fee_tax_policy.py b/tests/application/test_fee_tax_policy.py new file mode 100644 index 0000000..ce758bd --- /dev/null +++ b/tests/application/test_fee_tax_policy.py @@ -0,0 +1,76 @@ +"""Fee/tax policy tests (REQ-018 CHG-20260612-01, dcy-026, issue #24). + +Validates the single fee/tax policy point (`calculate_fee`/`calculate_tax`) +and the paper broker carrying the policy-computed sell transaction tax — the +correction for the `tax=0.0` hardcoding that under-counted realized P&L. +""" +from __future__ import annotations + +from trex_core.adapters.broker.paper import PaperBroker +from trex_core.application.broker_port import OrderRequest, calculate_fee, calculate_tax + + +# --- AC-6/AC-7: policy functions ------------------------------------------- + +def test_calculate_fee_same_rate_buy_and_sell(): + # 0.015 % commission on both sides (TREX_KIS_FEE_BPS=1.5 default) + assert calculate_fee("buy", 1_000_000) == 150.0 + assert calculate_fee("sell", 1_000_000) == 150.0 + + +def test_calculate_tax_sell_only(): + # 0.2 % securities transaction tax on sells (TREX_KRX_TAX_SELL_BPS=20.0) + assert calculate_tax("sell", 1_000_000) == 2000.0 + # buys are tax-free (KR equities) + assert calculate_tax("buy", 1_000_000) == 0.0 + + +def test_fee_and_tax_zero_for_nonpositive_amount(): + assert calculate_fee("sell", 0) == 0.0 + assert calculate_fee("buy", -100) == 0.0 + assert calculate_tax("sell", 0) == 0.0 + assert calculate_tax("sell", -100) == 0.0 + + +# --- AC-9: issue #24 four-fill regression (expected total 1,376.9원) -------- + +def test_issue_24_four_fill_regression(): + # (side, price) with qty=1 — from issue #24 evidence (2026-06-11). + fills = [ + ("buy", 291_500), + ("sell", 301_000), + ("buy", 295_000), + ("sell", 298_500), + ] + total_fee = sum(calculate_fee(side, amount) for side, amount in fills) + total_tax = sum(calculate_tax(side, amount) for side, amount in fills) + + assert round(total_fee, 1) == 177.9 + assert round(total_tax, 1) == 1199.0 + assert round(total_fee + total_tax, 1) == 1376.9 + + +# --- AC-7: paper broker carries policy tax (parity with KIS policy) --------- + +def _fill(broker, *, side, symbol, qty, price): + ack = broker.submit_order( + OrderRequest(slot_id="s1", side=side, symbol=symbol, qty=qty, limit_price=price) + ) + fs = broker.poll_fill(ack.broker_order_id, side=side, symbol=symbol) + assert fs.is_filled + return fs + + +def test_paper_sell_fill_carries_policy_tax(): + broker = PaperBroker(fill_after_polls=0) + fs = _fill(broker, side="sell", symbol="005930", qty=1, price=301_000) + assert fs.fee == calculate_fee("sell", 301_000) + assert fs.tax == calculate_tax("sell", 301_000) + assert fs.tax > 0.0 # the bug: previously hardcoded to 0.0 + + +def test_paper_buy_fill_has_zero_tax(): + broker = PaperBroker(fill_after_polls=0) + fs = _fill(broker, side="buy", symbol="005930", qty=1, price=291_500) + assert fs.fee == calculate_fee("buy", 291_500) + assert fs.tax == 0.0 diff --git a/trex_core/adapters/broker/kis.py b/trex_core/adapters/broker/kis.py index c56f197..cdc7b91 100644 --- a/trex_core/adapters/broker/kis.py +++ b/trex_core/adapters/broker/kis.py @@ -22,6 +22,7 @@ from trex_core.application.broker_port import ( OrderAck, OrderRequest, calculate_fee, + calculate_tax, ) _KST = timezone(timedelta(hours=9)) @@ -206,11 +207,17 @@ class KISBroker: row_odno = str(row.get("odno") or odno) execution_id = f"kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{row_odno}:{ccld_qty}" fill_price = float(avg) if avg else 0.0 - fee = calculate_fee(side, ccld_qty * fill_price) + # dcy-026: inquire-daily-ccld (output1) does not expose per-fill + # fee/tax fields, so use the shared policy estimate (same as paper). + # A future endpoint that returns broker-authoritative fee/tax would + # be parsed here first, with this as fallback (FR-3). + amount = ccld_qty * fill_price + fee = calculate_fee(side, amount) + tax = calculate_tax(side, amount) return FillStatus( state="filled", filled_qty=ccld_qty, avg_price=fill_price or None, - execution_id=execution_id, fee=fee, tax=0.0, raw=row, + execution_id=execution_id, fee=fee, tax=tax, raw=row, ) # 0 < ccld_qty < ord_qty if cancelled: @@ -223,11 +230,13 @@ class KISBroker: row_odno = str(row.get("odno") or odno) execution_id = f"kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{row_odno}:{ccld_qty}" fill_price = float(avg) if avg else 0.0 - fee = calculate_fee(side, ccld_qty * fill_price) + amount = ccld_qty * fill_price + fee = calculate_fee(side, amount) + tax = calculate_tax(side, amount) return FillStatus( state="partially_filled", filled_qty=ccld_qty, avg_price=fill_price or None, - execution_id=execution_id, fee=fee, tax=0.0, + execution_id=execution_id, fee=fee, tax=tax, cancelled=True, raw=row, ) return FillStatus(state="partially_filled", filled_qty=ccld_qty, diff --git a/trex_core/adapters/broker/paper.py b/trex_core/adapters/broker/paper.py index 8b48224..1a8201e 100644 --- a/trex_core/adapters/broker/paper.py +++ b/trex_core/adapters/broker/paper.py @@ -17,6 +17,7 @@ from trex_core.application.broker_port import ( OrderAck, OrderRequest, calculate_fee, + calculate_tax, ) @@ -84,10 +85,12 @@ class PaperBroker: # Deterministic execution_id (dcy-004 OQ-3): # paper:{broker_order_id}:{filled_qty} execution_id = f"paper:{o.oid}:{o.filled}" - fee = calculate_fee(o.side, o.filled * o.price) + amount = o.filled * o.price + fee = calculate_fee(o.side, amount) + tax = calculate_tax(o.side, amount) return FillStatus( state="filled", filled_qty=o.filled, avg_price=o.price, - execution_id=execution_id, fee=fee, tax=0.0, + execution_id=execution_id, fee=fee, tax=tax, ) return FillStatus(state="partially_filled", filled_qty=o.filled, avg_price=o.price) diff --git a/trex_core/application/broker_port.py b/trex_core/application/broker_port.py index 6a32aed..31a5a6e 100644 --- a/trex_core/application/broker_port.py +++ b/trex_core/application/broker_port.py @@ -7,6 +7,7 @@ trex_core.adapters.broker. Fills are observed asynchronously via poll_fill """ from __future__ import annotations +import os from dataclasses import dataclass, field from decimal import Decimal from typing import Any, Optional, Protocol @@ -48,8 +49,11 @@ class FillStatus: filled_qty: int = 0 avg_price: Optional[float] = None # REQ-018: deterministic execution_id + broker-computed fee/tax carried - # all the way to the ledger writer. tax defaults to 0 (precise tax model - # is a follow-up Discovery, dcy-004 OQ-3). + # all the way to the ledger writer. fee = calculate_fee(side, amount), + # tax = calculate_tax(side, amount) (sell transaction tax; buy = 0). + # dcy-026/CHG-20260612-01 closed the dcy-004 OQ-3 follow-up: tax is no + # longer hardcoded to 0 — adapters MUST compute it via calculate_tax. + # The field defaults to 0.0 only for non-fill states (accepted/rejected/...). execution_id: Optional[str] = None fee: float = 0.0 tax: float = 0.0 @@ -112,20 +116,35 @@ class BrokerPort(Protocol): ... -# --- shared fee model (REQ-018, orbit calculate_fee pattern) ----------------- -# Conservative KR equities approximation (bps). Brokers can override, but we -# need a single default so paper and KIS produce comparable ledger entries. -_FEE_BPS_BUY = 1.5 # 0.015 % broker commission on buys -_FEE_BPS_SELL = 1.5 # 0.015 % broker commission on sells (tax is tracked separately) +# --- shared fee/tax policy (REQ-018, dcy-026, orbit calculate_fee pattern) ---- +# Single policy point so paper and KIS produce comparable ledger entries +# (NFR-2/NFR-3). Rates are bps (1 bp = 0.01 %) and configurable via env so a +# rate change reaches every broker/ledger path from one place. The variable +# names carry KIS/KRX domain labels, but the values are shared by all adapters +# (the paper double simulates KIS costs). dcy-026 OQ-1/OQ-2 Closed. +_FEE_BPS = float(os.environ.get("TREX_KIS_FEE_BPS", "1.5")) # 0.015 % broker commission (buy & sell) +_TAX_BPS_SELL = float(os.environ.get("TREX_KRX_TAX_SELL_BPS", "20.0")) # 0.2 % KRX sell transaction tax def calculate_fee(side: str, amount: float) -> float: """Estimate broker fee for a notional `amount` (qty * price). Mirrors orbit `calculate_fee(side, amount)` shape — a single point of - fee policy so all brokers and the ledger agree on the number. + fee policy so all brokers and the ledger agree on the number. Buy and sell + share the same commission rate (`TREX_KIS_FEE_BPS`). """ if amount <= 0: return 0.0 - bps = _FEE_BPS_BUY if side == "buy" else _FEE_BPS_SELL - return round(amount * bps / 10_000.0, 4) + return round(amount * _FEE_BPS / 10_000.0, 4) + + +def calculate_tax(side: str, amount: float) -> float: + """Estimate transaction tax for a notional `amount` (qty * price). + + KR equities: buys are tax-free; sells incur securities transaction tax + (`TREX_KRX_TAX_SELL_BPS`, default 0.2 %). Single policy point alongside + `calculate_fee` (dcy-026 — replaces the old hardcoded `tax=0.0`). + """ + if amount <= 0 or side != "sell": + return 0.0 + return round(amount * _TAX_BPS_SELL / 10_000.0, 4)