# req-070: slippage 보호 지정가 집행 — entry_price 기준 실효 지정가 산출 - Status: Implemented - Type: Integration - Priority: High - Owner: project maintainers ## Intent - `slippage_pct` 가 지정된 주문을 dispatch 할 때 `entry_price*(1±pct)` 실효 지정가로 제출해 과도한 불리 체결을 막는다. `entry_price` 없으면 무시(순수 시장가는 슬리피지 기준 없음). (dcy-022 FR-3) ## Requirement - `_dispatch` 가 슬롯의 `slippage_pct` 를 읽어 실효 지정가를 산출한 뒤 **KRX 호가단위(REQ-083)로 스냅**한다. 방향은 **상한 준수**: 매수 = `floor_to_tick(limit_price * (1 + slippage_pct/100))` (내림 — slippage 상한 미초과), 매도 = `ceil_to_tick(limit_price * (1 - slippage_pct/100))` (올림 — 매도 하한 미초과). band<1tick 엣지에서 미체결을 막기 위해 하한/상한을 보장한다: 매수 실효가 = `max(floor_to_tick(...), ceil_to_tick(limit_price))`, 매도 실효가 = `min(ceil_to_tick(...), floor_to_tick(limit_price))`. `limit_price`(= `entry_price`)가 None 이면 `slippage_pct` 를 무시하고 시장가 주문 제출(기존 동작 보존, 스냅 없음). `slippage_pct` 없는 슬롯은 기존 `limit_price` 직접 사용(회귀 없음). 주식 전용 — ETF/ETN 비주식 호가체계는 범위 밖(dcy-030 OQ-1). ## Acceptance Criteria 1. Given 슬롯에 `limit_price=10000`, `slippage_pct=1.0`, side=buy, When dispatch, Then OrderRequest.limit_price = `floor_to_tick(10000*1.01)` = `floor_to_tick(10100)` = 10100 (상한 준수 내림). 2. Given 슬롯에 `limit_price=10000`, `slippage_pct=1.0`, side=sell, When dispatch, Then OrderRequest.limit_price = `ceil_to_tick(10000*0.99)` = `ceil_to_tick(9900)` = 9900 (하한 준수 올림). 3. Given `limit_price=None`(시장가), `slippage_pct=1.0`, When dispatch, Then OrderRequest.limit_price = None(시장가 — slippage·스냅 무시). 4. Given `slippage_pct=None`, `limit_price=10000`, When dispatch, Then OrderRequest.limit_price = 10000(기존 동작, 회귀 없음). 5. Given `slippage_pct=0.0`, When dispatch, Then 실효 지정가 = `floor_to_tick(limit_price)`(0% 슬리피지 = 기준가의 유효 호가). 6. Given 기존 매수/매도 슬롯(slippage_pct 없음), When dispatch, Then 기존 동작과 동일(전체 회귀 없음). 7. Given `limit_price=147700`, `slippage_pct=0.5`, side=buy, When dispatch, Then `floor_to_tick(148438.5)` = 148400 (issue #28 케이스, 유효 호가·상한 미초과). 8. Given `limit_price=54500`, `slippage_pct=0.5`, side=buy, When dispatch, Then `floor_to_tick(54772.5)` = 54700 (issue #28 케이스). 9. Given band<1tick (예: `limit_price=147700`, `slippage_pct=0.01`, buy), When dispatch, Then 매수 실효가 = `max(floor_to_tick(147714.77), ceil_to_tick(147700))` ≥ entry 유효 호가 (미체결 방지), 그리고 slippage 상한 미초과. 10. Given 임의 매수/매도 + slippage, When dispatch, Then 결과는 유효 KRX 호가단위 배수이고 매수 ≤ `limit*(1+pct/100)` / 매도 ≥ `limit*(1-pct/100)`. ## Impacted Area - Docs: docs/runtime-flows.md(slippage dispatch + 호가단위 스냅 흐름), docs/project-structure.md(신규 tick 유틸 — REQ-083) - Modules: trex_core/application/execution.py(`_dispatch` slippage 계산 + 호가단위 스냅), REQ-083 tick 유틸 소비 - Tests: tests/application/test_execution.py(slippage dispatch + 스냅 방향·경계·band0인 매수 한계가가 호가단위를 위반해 KIS `주식주문호가단위 오류`로 전량 거부됨(issue #28). 호가단위 스냅으로 거부를 제거하되, `slippage_pct`를 리스크 상한으로 취급해 스냅이 상한을 넘기지 않도록 매수=내림/매도=올림으로 정정한다. #### Acceptance Criteria Delta - Updated: - AC-1: 매수 = `floor_to_tick(limit*(1+pct/100))` (상한 준수 내림). 예: `limit=10000,pct=1.0` → `floor_to_tick(10100)=10100`. - AC-2: 매도 = `ceil_to_tick(limit*(1-pct/100))` (하한 준수 올림). 예: `limit=10000,pct=1.0` → `ceil_to_tick(9900)=9900`. - Added: - AC-7: `limit=147700,pct=0.5,buy` → `floor_to_tick(148438.5)=148400` (issue #28 케이스). - AC-8: `limit=54500,pct=0.5,buy` → `floor_to_tick(54772.5)=54700` (issue #28 케이스). - AC-9 (band<1tick 하한 보장): 매수 실효가 = `max(floor_to_tick(limit*(1+pct/100)), ceil_to_tick(limit))`; 매도 실효가 = `min(ceil_to_tick(limit*(1-pct/100)), floor_to_tick(limit))`. slippage<1tick이어도 최소 entry 유효 호가 보장. - AC-10: 스냅 결과는 항상 유효 호가단위 배수이고, 매수 ≤ `limit*(1+pct/100)` / 매도 ≥ `limit*(1-pct/100)` (상한·하한 미초과). - Removed: - (없음 — AC-3·4·5·6 시장가/무slippage/0% 회귀 기준 유지) #### Impacted Area Delta - Docs: - docs/runtime-flows.md(주문 집행 슬리피지 dispatch에 호가단위 스냅 단계 추가), docs/project-structure.md(신규 tick 유틸 — REQ-083) - Modules: - trex_core/application/execution.py(`_dispatch` 스냅 적용), REQ-083 tick 유틸 소비 - Tests: - tests/application/test_execution.py(슬리피지 스냅 방향·경계·band