# BAT-032 Planning - Status: done - Batch: BAT-032 ## Milestones | ID | 목표 | 연결 REQ | |---|---|---| | M1 | 실행 정책 계약·파싱·슬롯 저장 | REQ-068, REQ-061(CHG-05) | | M2 | slippage 보호 지정가 dispatch | REQ-070, REQ-013(CHG-03 partial) | | M3 | timeout 집행 + race 처리 | REQ-069, REQ-013(CHG-03 partial) | | M4 | 운영자 cancel 백스톱 CLI + control RPC | REQ-071 | | M5 | baseline 문서 정합 | REQ-072 | ## M1 상세 (실행 정책 계약·파싱·슬롯 저장) - `Slot` 모델: `timeout_sec: Optional[int]`, `slippage_pct: Optional[float]` 필드 추가. `EXPECTED_SLOT_FIELDS` 갱신. - Migration 012: `ALTER TABLE daily_slot ADD COLUMN IF NOT EXISTS timeout_sec INTEGER; ADD COLUMN IF NOT EXISTS slippage_pct FLOAT;` - `BuyIntent`/`SellIntent`: `timeout_sec`/`slippage_pct` 추가. - `IntentService`: `__init__` 에 `default_order_timeout_sec: Optional[int] = None` 추가. `_admit_buy`/`_admit_sell` 에서 effective_timeout = intent.timeout_sec ?? config_default → transit_slot 에 전달. - `SlotService.transit_slot` kwargs: `timeout_sec`/`slippage_pct` 전달. `_apply_transition_fields` 에서 PENDING_* 진입 시 set, 탈출(→HOLDING/→INACTIVE) 시 clear. - `messaging_handlers.py`: `timeout_sec=_opt_int(body.get("timeout_sec"))`, `slippage_pct=_opt_float(body.get("slippage_pct"))` 파싱 추가. - `DaemonInstanceConfig`: `default_order_timeout_sec: Optional[int] = None` 추가, from_toml 파싱. - `DaemonConfig` + daemon runtime 배선: IntentService 에 default_order_timeout_sec 전달. ## M2 상세 (slippage 보호 지정가 dispatch) - `execution._dispatch`: limit 계산 후 slippage 적용: ```python import math if limit is not None and slot.slippage_pct: factor = 1 + slot.slippage_pct/100 if side == "buy" else 1 - slot.slippage_pct/100 limit = int(math.ceil(limit * factor)) if side == "buy" else int(math.floor(limit * factor)) ``` - `_log.debug(...)` 로 slippage 적용 기록. ## M3 상세 (timeout 집행) - `ExecutionResult`: `timeout_cancelled: int = 0` 추가. - `_poll` 의 else branch(`_check_stale_pending` 호출)에서 timeout 체크 추가: ```python self._check_stale_pending(slot, now, r) # Alert-only (REQ-014, no change) self._try_timeout_cancel(slot, now, r) # NEW: cancel if timeout_sec exceeded ``` - `_try_timeout_cancel`: `slot.timeout_sec` 있고 경과 시간 초과 시 → `broker.cancel_order(...)` best-effort → `r.timeout_cancelled += 1` → WARNING 로그. 실패 시 WARNING, 상태 불변. ## M4 상세 (운영자 cancel 백스톱) - `messaging_handlers.make_control_rpc_handler`: `cancel_pending` 액션 추가. `cancel_pending_fn` 콜백(slot_id → bool) 주입. - `execution.ExecutionService.cancel_pending_slot(slot_id)`: 슬롯 조회 → PENDING_* 이면 `broker.cancel_order` 송신. 슬롯 직접 전이 없음. - `cli.py`: `cancel ` 서브커맨드 추가. `--dry-run`/`--yes`/`--live` 안전장치. control RPC 경유. - daemon.py 배선: make_control_rpc_handler 에 cancel_pending_fn 전달. ## M5 상세 (baseline 문서) - `docs/interface-contract.md`: buy/sell payload 에 `timeout_sec`/`slippage_pct` 추가. - `docs/runtime-flows.md`: 집행 루프·dispatch 섹션에 timeout 취소·slippage·cancel 백스톱 흐름 추가. ## 위험 요소 - **R-1 (취소-체결 race)**: cancel_order 송신 후 즉시 롤백하지 않고 다음 poll 수렴 → 기존 is_terminal_reject/is_partial_cancel 경로 재사용 완화. - **R-2 (부분 체결 보존)**: timeout cancel → is_partial_cancel → 기존 `_on_partial_cancel` 선례(dcy-013) 재사용. - **R-3 (단일 writer)**: cancel CLI 는 cancel_order 만 송신; 슬롯 전이는 집행 루프 단독 → 불변식 보존.