# BAT-016 Design - Status: confirmed - Batch: BAT-016 - Profile: standard ## 설계 목표 - 매도 주문이 매수 지정가(`entry_price`)로 나가는 버그(issue #10)를 도메인 모델 차원에서 구조적으로 제거한다. - released 집행 경로 변경·컬럼 rename 마이그레이션을 회귀 없이 cutover 한다. ## 1. 슬롯 가격 필드 의미 통일 (REQ-002) ### rename - `Slot.entry_price` → `Slot.limit_price` (side 무관 "현재 pending 주문 지정가"). `EXPECTED_SLOT_FIELDS` 동시 반영(컬럼 계약 가드가 곧 검출). - `transit_slot`/`_apply_transition_fields` 의 `entry_price` 파라미터 → `limit_price`. ### limit_price 생애주기 (FR-1, 핵심) - 결정: limit_price 는 **pending 상태에서만 유효**. `_apply_transition_fields` 의 generic 경로에서: - `to_state ∈ {PENDING_BUY, PENDING_SELL}` → `limit_price = 입력값 if not None else slot.limit_price`(set on entry). - `to_state ∈ {HOLDING, CLOSED}` → `limit_price = None`(clear on exit). - `to_state == IDLE`(롤백/복구) → 기존 reset 분기에서 이미 None clear. - 효과: PENDING_BUY→HOLDING 에서 매수 지정가가 clear 되므로, 이후 HOLDING→PENDING_SELL 에서 매도 가격 미전달이면 slot.limit_price 는 None → 시장가. 매수 지정가가 매도로 누수될 경로가 구조적으로 없음. - 전이 매트릭스: - IDLE→PENDING_BUY: set(매수 지정가) / PENDING_BUY→HOLDING: clear / HOLDING→PENDING_SELL: set(매도 지정가 또는 None) / PENDING_SELL→CLOSED: clear / *→IDLE: clear. ### SlotService.sell (REQ-036) - `sell(slot_id, actor, reason, *, limit_price=None)`. default None → 자율 매도(safety-sell/kill-switch)는 가격 없이 호출 → 시장가. transit_slot 에 limit_price 전달. ## 2. dispatch 가격 결정 (REQ-013) - `_dispatch`: side 분기 제거. `limit = int(slot.limit_price) if slot.limit_price else None`; `order_type = "00" if limit is not None else "01"`. 매수가 재사용 경로 삭제. ## 3. sell intent 계약 (REQ-007) - `SellIntent.limit_price: Optional[float] = None`. `_admit_sell` → `transit_slot(..., limit_price=intent.limit_price)`. - `_admit_buy`: 기존 `entry_price=intent.entry_price`(BuyIntent 외부 필드 유지) → 슬롯 `limit_price=intent.entry_price` 로 매핑(매수 동작 동일). - 단일 메커니즘(가격 유무 분기, OQ-2): reason 별 코드 차등 없음. ## 4. 메시지 경계 (REQ-007, NFR-3) - sell 핸들러: `limit_price=_opt_float(body.get("limit_price"))` 추가. - buy 핸들러: 외부 payload 필드명 `entry_price` 유지(BuyIntent.entry_price). - slot read(`_slot_to_dict`): 외부 read 키 `"entry_price"` 유지하되 내부 `s.limit_price` 로 매핑(계약 호환 — 외부 필드명 rename 은 본 주기 밖). ## 5. persistence + 마이그레이션 (REQ-006) - `006_rename_entry_price_to_limit_price.sql`: `ALTER TABLE daily_slot RENAME COLUMN entry_price TO limit_price` 를 idempotent DO-block(컬럼 존재 && 신컬럼 부재 시에만)으로. 글롭 재적용 모델에서 안전. 순수 rename → 기존 행 데이터 보존(S-3). - `postgresql.py`: `_SLOT_COLUMNS`/update_slot/_row_to_slot 의 entry_price → limit_price. ## 6. 컬럼 계약 가드 (REQ-003) - `test_contract.py`(도메인 필드셋)·`test_migration_and_drift.py`(DB 컬럼 = EXPECTED_SLOT_FIELDS)는 상수 참조라 rename 자동 반영; PG 게이트에서 실 컬럼명 limit_price 검증. ## 7. baseline 문서 (REQ-015) - runtime-flows: dispatch 가격 결정 = slot.limit_price 단일 필드 + limit_price 생애주기. - interface-contract: sell payload optional limit_price + ORD_DVSN 매핑. - data-model: 슬롯 limit_price 컬럼 의미/생애주기. ## 하위호환 / 회귀 통제 - 외부 buy/sell payload·slot read 필드명 유지(NFR-3) → 전략 모듈 계약 무파괴. - sell limit_price optional default None → 기존 재량 매도 호출 시장가로 안전 동작. - 전체 회귀 green 이 rename 호출부 누락 부재의 1차 게이트. ## 테스트 설계 - 신규 `tests/application/test_sell_pricing.py`(recording paper broker 로 OrderRequest 직접 검증): - (a) 지정가 매수 슬롯 + 가격 미전달 매도 → sell order_type "01"/limit None(entry_price 미사용 — 핵심 버그 검출). - (b) 지정가 매수 슬롯 + 가격 전달 매도 → "00"/그 가격. - (c) SlotService.sell(가격 없음) → 시장가 + closed(S-2). - (d) 시장가 매수 슬롯 매도 → 시장가(회귀 안전). - `test_service.py`: PENDING_BUY→HOLDING 시 limit_price clear 단언 추가.