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
+13 -4
View File
@@ -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,
+5 -2
View File
@@ -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)