feat(safety-sell): dcy-007 core 자율 안전 매도 — BAT-010 기반 + BAT-011 트리거
dcy-007(core 자율 안전 매도) 전체 delivery. BAT-010 기반 인프라가 미커밋 상태였고 BAT-011 트리거 레이어와 공유 파일에서 섞여 있어 한 커밋으로 묶음. BAT-010 (REQ-034/035/036/041, release-candidate): - WatchdogStore(application/watchdog.py) + snapshot RPC 워치독 갱신 - Slot stop_loss_price/take_profit_price 필드 + 004_add_price_thresholds.sql - SlotService.sell() 자율 매도 내부 경로 + idempotency - SafetySellConfig env 5종 파싱 BAT-011 (REQ-037/038/039/040/042, release-candidate): - SafetySellService(application/safety_sell.py): forced_close 만기 / stop-loss· take-profit KIS polling / 장 마감 강제 청산 + trex-safety-sell 7번째 스레드 - BrokerPort.get_current_price + KIS inquire-price(TTTC8397R/VTTC8397R, stck_prpr) + paper stub - SlotService.update_thresholds() + intent RPC update_thresholds 액션 (부분 갱신/null 해제, 워치독 last_seen_at 리셋) - baseline 3문서 정합(interface-contract/runtime-flows/project-structure) 거버넌스: REQ-034~042 Implemented 동기화, BAT-010/011 release-candidate, dcy-007 discovery 포함. 검증: 신규 38 테스트 green, 단위 236 passed/23 skipped, 인프라 게이트(PG+RabbitMQ) 259 passed(마이그레이션 001~004 round-trip 포함), ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# BAT-010 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-010
|
||||
|
||||
## Architecture Summary
|
||||
BAT-010은 safety-sell 기능의 기반 레이어 세 가지를 추가한다.
|
||||
|
||||
1. **WatchdogStore** (`application/watchdog.py` 신규): `dict[str, datetime]` in-memory store. snapshot RPC handler가 응답 슬롯의 `last_seen_at`을 갱신하고 BAT-011 트리거 루프가 만료 판정에 사용한다.
|
||||
2. **도메인 필드 확장** (`models.py`, `postgresql.py`, SQL 마이그레이션): `stop_loss_price`, `take_profit_price` `Optional[Decimal]` additive 추가. BAT-011 KIS polling이 트리거 기준으로 사용한다.
|
||||
3. **SlotService.sell()** (`domain/slot/service.py`): RabbitMQ 우회 내부 매도 경로. BAT-011의 모든 트리거 루프(forced_close, KIS polling, 장 마감)가 이 메서드를 호출한다.
|
||||
|
||||
## Changed Areas
|
||||
- Docs:
|
||||
- 없음 (baseline 문서 갱신은 REQ-042 / BAT-011에서 처리)
|
||||
- Modules:
|
||||
- `trex_core/runtime/config.py` — `DaemonConfig`에 safety-sell env vars 5종 파싱 추가
|
||||
- `trex_core/domain/slot/models.py` — `stop_loss_price: Optional[Decimal]`, `take_profit_price: Optional[Decimal]` 추가
|
||||
- `trex_core/adapters/persistence/postgresql.py` — 두 필드 read/write 포함
|
||||
- `trex_core/adapters/persistence/sql/` — 마이그레이션 스크립트 신규 추가
|
||||
- `trex_core/application/intents.py` — `IntentIn`에 `stop_loss_price`, `take_profit_price` Optional 필드 추가
|
||||
- `trex_core/application/messaging_handlers.py` — `make_snapshot_rpc_handler` 확장(`last_seen_at` 갱신) + intent RPC payload 파싱 확장(price fields)
|
||||
- `trex_core/application/watchdog.py` (신규) — `WatchdogStore` class
|
||||
- `trex_core/domain/slot/service.py` — `SlotService.sell(slot_id, actor, reason)` 추가
|
||||
- Interfaces:
|
||||
- `SlotService.sell(slot_id: str, actor: str, reason: str) -> None` (신규 내부 인터페이스)
|
||||
- `WatchdogStore.update(slot_id: str) -> None` (신규)
|
||||
- `WatchdogStore.is_expired(slot_id: str, timeout_s: int) -> bool` (신규 — BAT-011 트리거 루프 계약)
|
||||
- `WatchdogStore.in_grace_period(grace_s: int) -> bool` (신규)
|
||||
|
||||
## Key Decisions
|
||||
1. **WatchdogStore를 `watchdog.py` 신규 모듈로 분리**: `portfolio.py`는 snapshot 조회·직렬화 책임이 있어 watchdog 관심사(타임스탬프 추적·만료 판정)를 추가하면 단일 책임 위반. 신규 모듈이 독립 테스트와 BAT-011 참조에 유리하다.
|
||||
2. **REQ-041 env vars 5종 전체를 BAT-010에서 파싱 완성**: `TREX_FORCED_CLOSE_CHECK_INTERVAL`·`TREX_STOP_LOSS_POLL_INTERVAL`·`TREX_MARKET_CLOSE_LIQUIDATION_TIME`은 BAT-010에서 사용하지 않지만, REQ-041을 두 batch에 분리하면 REQ 단위 거버넌스 trace가 깨진다. 파싱만 완성하고 BAT-011이 config 값을 참조한다.
|
||||
3. **WatchdogStore 동시성은 Python GIL에 의존**: `dict[str, datetime]` 단일 dict의 Python 레벨 read/write는 GIL 보장 하에 원자적. 별도 `threading.Lock` 불필요. grace period 판정: `datetime.utcnow() - self.started_at < timedelta(seconds=grace_s)` 단순 비교.
|
||||
4. **SlotService.sell()은 transit_slot() 재사용**: 기존 `transit_slot(slot_id, from_state=HOLDING, to_state=PENDING_SELL, ...)` advisory lock + outbox 경로를 그대로 재사용. 신규 로직을 최소화해 BAT-004 검증 coverage를 그대로 이어받는다.
|
||||
|
||||
## Edge Cases
|
||||
- grace period 중 `is_expired()` 호출: `in_grace_period()` True이면 store에 없는 슬롯 포함 전체 만료 판정 유예.
|
||||
- 데몬 재시작 시 watchdog store: in-memory 초기화 + `started_at` 재설정으로 grace period 자동 재시작.
|
||||
- `sell()` 호출 시 이미 `pending_sell` 상태: `transit_slot` 호출 전 상태 조회 → silent return (중복 매도 intent 제출 없음).
|
||||
- `sell()` 호출 시 `idle`·`cancelled` 등 비정상 상태: `SlotStateError`(또는 동등 예외) raise, 슬롯 상태 불변.
|
||||
- 마이그레이션 기존 row: `ALTER TABLE slots ADD COLUMN IF NOT EXISTS stop_loss_price NUMERIC` 패턴으로 멱등성 보장. 기존 row `NULL` 유지.
|
||||
|
||||
## Architecture Impact
|
||||
- runtime-flows: snapshot RPC handler → watchdog `last_seen_at` 갱신 흐름 추가 (BAT-011 REQ-042에서 문서화)
|
||||
- project-structure: `watchdog.py` 신규 모듈 추가 (BAT-011 REQ-042에서 문서화)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: BAT-011 REQ-042에서 처리 (`watchdog.py` 신규 모듈 반영)
|
||||
- runtime-flows.md: BAT-011 REQ-042에서 처리 (snapshot RPC → watchdog update + sell() 내부 경로 반영)
|
||||
@@ -0,0 +1,43 @@
|
||||
# BAT-010 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-010
|
||||
|
||||
## Plan Summary
|
||||
- REQ-041 → REQ-035 → REQ-034 → REQ-036 순서로 구현. 각 단계 unit test green 확인 후 다음으로 진행.
|
||||
|
||||
## Changed Files
|
||||
|
||||
### 신규 파일
|
||||
- `trex_core/application/watchdog.py` — WatchdogStore class (REQ-034)
|
||||
- `trex_core/adapters/persistence/sql/004_add_price_thresholds.sql` — additive DB migration (REQ-035)
|
||||
- `tests/runtime/test_safety_sell_config.py` — REQ-041 AC 검증 4종
|
||||
- `tests/application/test_watchdog.py` — REQ-034 AC 검증 11종
|
||||
- `tests/application/test_snapshot_rpc_watchdog.py` — REQ-034 handler 통합 4종
|
||||
- `tests/domain/slot/test_service_sell.py` — REQ-036 AC 검증 6종
|
||||
- `tests/domain/slot/test_price_threshold_fields.py` — REQ-035 AC 검증 7종
|
||||
|
||||
### 기존 파일 수정
|
||||
- `trex_core/runtime/config.py` — SafetySellConfig dataclass 추가 + DaemonConfig 5종 env var 파싱 (REQ-041)
|
||||
- `trex_core/domain/slot/models.py` — stop_loss_price / take_profit_price Optional[Decimal] 필드 추가 + EXPECTED_SLOT_FIELDS 갱신 (REQ-035)
|
||||
- `trex_core/adapters/persistence/postgresql.py` — _SLOT_COLUMNS 갱신, update_slot SET절 확장, _row_to_slot 인덱스 조정 (REQ-035)
|
||||
- `trex_core/application/intents.py` — BuyIntent에 price fields + Decimal import (REQ-035)
|
||||
- `trex_core/domain/slot/service.py` — transit_slot에 stop_loss_price / take_profit_price 파라미터 추가, _apply_transition_fields 확장(reset + preserve), sell() 메서드 신설, Decimal import (REQ-035, REQ-036)
|
||||
- `trex_core/application/messaging_handlers.py` — WatchdogStore import + SlotState import + make_snapshot_rpc_handler watchdog_store= 파라미터 + 홀딩 슬롯 update 로직 + buy intent payload _opt_decimal 파싱 (REQ-034, REQ-035)
|
||||
|
||||
## Execution Log
|
||||
1. M1 — REQ-041: SafetySellConfig + DaemonConfig.from_env() 5종 파싱. test_safety_sell_config.py 4종 green.
|
||||
2. M2 — REQ-035: models.py 필드 추가 + EXPECTED_SLOT_FIELDS 갱신 → test_contract.py 통과. postgresql.py _SLOT_COLUMNS / update_slot / _row_to_slot 갱신. SQL 마이그레이션 004 생성. intents.py BuyIntent 확장. service.py transit_slot + _apply_transition_fields 확장. test_price_threshold_fields.py 7종 green.
|
||||
3. M3 — REQ-034: watchdog.py 신규. messaging_handlers.py make_snapshot_rpc_handler 확장. test_watchdog.py 11종 green. test_snapshot_rpc_watchdog.py 4종 green.
|
||||
4. M4 — REQ-036: service.py sell() 신설. test_service_sell.py 6종 green.
|
||||
5. M5 — 전체 회귀 95 passed (httpx 미설치 환경 기인 test_daemon_no_http_surface 1건 기존 실패, BAT-010 무관).
|
||||
|
||||
## Validation
|
||||
- 신규 테스트: 32종 100% green
|
||||
- 회귀 테스트: 95 passed (domain/ + runtime/ + application/ 대상, httpx 불필요 범위)
|
||||
- test_contract.py: EXPECTED_SLOT_FIELDS ↔ dataclasses.fields(Slot) 계약 통과 (price fields 포함)
|
||||
- `WatchdogStore.in_grace_period()` + `is_expired()` 조합 패턴 단위 테스트로 고정
|
||||
|
||||
## Remaining Risks
|
||||
- R-2 (Medium → Low): DB 마이그레이션은 실 PostgreSQL 없이 검증 불가. 004 migration이 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 패턴 사용 → 멱등 보장. 실 DB 검증은 BAT-011 환경 포함 후 수행.
|
||||
- R-3 (Low): test_daemon_no_http_surface 기존 실패 — httpx 미설치 환경 이슈. BAT-010 변경 무관. 런타임 환경에서 httpx 설치 시 해소.
|
||||
@@ -0,0 +1,35 @@
|
||||
# BAT-010: safety-sell 기반 인프라 (watchdog + 가격 임계값 필드 + SlotService.sell)
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-007
|
||||
- Included REQ: REQ-034, REQ-035, REQ-036, REQ-041
|
||||
- Excluded REQ: REQ-037, REQ-038, REQ-039, REQ-040, REQ-042 (safety-sell 트리거 레이어 — BAT-011 예정)
|
||||
|
||||
## Summary
|
||||
- dcy-007(core 자율 안전 매도) 기반 인프라 레이어를 delivery한다. per-slot 워치독 store(REQ-034), 슬롯 가격 임계값 도메인 필드 + DB 마이그레이션(REQ-035), 자율 매도 내부 경로 `SlotService.sell()`(REQ-036), safety-sell 환경 변수 전체 파싱(REQ-041)을 포함한다. 트리거 루프(forced_close/KIS polling/장 마감)와 `update_thresholds` RPC는 BAT-011에서 처리한다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `WatchdogStore` 신규 모듈(`application/watchdog.py`) + `make_snapshot_rpc_handler` 확장 (REQ-034)
|
||||
- `stop_loss_price`, `take_profit_price` Optional[Decimal] 도메인 필드 + PostgreSQL additive 마이그레이션 (REQ-035)
|
||||
- `SlotService.sell(slot_id, actor, reason)` + idempotency (REQ-036)
|
||||
- `DaemonConfig` env vars 5종 파싱: `TREX_WATCHDOG_TIMEOUT`, `TREX_WATCHDOG_GRACE_PERIOD`, `TREX_FORCED_CLOSE_CHECK_INTERVAL`, `TREX_STOP_LOSS_POLL_INTERVAL`, `TREX_MARKET_CLOSE_LIQUIDATION_TIME` (REQ-041)
|
||||
- Out of Scope:
|
||||
- forced_close_at 루프 — BAT-011 (REQ-037)
|
||||
- KIS 현재가 polling — BAT-011 (REQ-038)
|
||||
- 장 마감 청산 루프 — BAT-011 (REQ-039)
|
||||
- update_thresholds intent RPC — BAT-011 (REQ-040)
|
||||
- baseline 문서 정렬 — BAT-011 (REQ-042)
|
||||
- 가격 임계값 논리 유효성 검증(take_profit > stop_loss 등) — REQ-035 Notes 명시
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- BAT-010은 외부 API(KIS) 호출이 없고 신규 daemon thread를 추가하지 않는다. 단위 테스트와 persistence 통합 테스트만으로 모든 AC를 검증 가능한 레이어다.
|
||||
- REQ-041의 env vars 5종 중 BAT-010에서 실제 사용되는 것은 `TREX_WATCHDOG_TIMEOUT` + `TREX_WATCHDOG_GRACE_PERIOD` 2개다. 나머지 3종(`TREX_FORCED_CLOSE_CHECK_INTERVAL`, `TREX_STOP_LOSS_POLL_INTERVAL`, `TREX_MARKET_CLOSE_LIQUIDATION_TIME`)은 파싱만 완성하고 BAT-011에서 참조한다. REQ-041을 두 batch에 분리하지 않기 위한 결정(design.md Key Decision 2 참조).
|
||||
- WatchdogStore 구현 위치: `trex_core/application/watchdog.py` 신규 모듈(design.md Key Decision 1 참조).
|
||||
@@ -0,0 +1,42 @@
|
||||
# BAT-010 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-010
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-041 (Configuration): safety-sell 환경 변수 5종 파싱 — `TREX_WATCHDOG_TIMEOUT`(s, 기본 120), `TREX_WATCHDOG_GRACE_PERIOD`(s, 기본 60), `TREX_FORCED_CLOSE_CHECK_INTERVAL`(s, 기본 60), `TREX_STOP_LOSS_POLL_INTERVAL`(s, 기본 30), `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST, optional)
|
||||
- REQ-035 (Data): `stop_loss_price` · `take_profit_price` `Optional[Decimal]` 도메인 필드 + PostgreSQL additive 마이그레이션
|
||||
- REQ-034 (Interface): per-slot 워치독 — `WatchdogStore(dict[slot_id, datetime])` + snapshot RPC handler 확장 + grace period
|
||||
- REQ-036 (Interface): `SlotService.sell(slot_id, actor, reason)` — `holding→pending_sell` 전이 + idempotency
|
||||
|
||||
## Delivery Plan
|
||||
1. REQ-041: `runtime/config.py`에 safety-sell env vars 5종 파싱 추가 (`DaemonConfig` 확장). 파싱 단위 테스트.
|
||||
2. REQ-035: `domain/slot/models.py`에 두 필드 추가 → `adapters/persistence/postgresql.py` read/write 포함 → SQL 마이그레이션 스크립트 작성 → `application/intents.py` + `messaging_handlers.py` payload 파싱 추가. 필드 저장·조회 round-trip 테스트 + 마이그레이션 멱등성 테스트.
|
||||
3. REQ-034: `application/watchdog.py` 신규 작성 (`WatchdogStore` class, grace period 판정) → `make_snapshot_rpc_handler` 확장 (응답 슬롯 `last_seen_at` 갱신). 워치독 판정 단위 테스트 (만료/비만료/grace period 시나리오).
|
||||
4. REQ-036: `domain/slot/service.py`에 `sell(slot_id, actor, reason)` 추가 (`transit_slot` 기반, idempotency). 단위 테스트 3종(holding→pending_sell, pending_sell 중복 호출, idle 오류).
|
||||
5. 전체 회귀 테스트 green + ruff lint clean.
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-007/REQ-022: `make_snapshot_rpc_handler` 기존 구현 — 확장 대상
|
||||
- BAT-008/REQ-027: `SlotService` 기존 구조 + `transit_slot` 패턴 — `sell()` 기반
|
||||
- BAT-004/REQ-011~012: `PaperBroker` execution 경로 — `sell()` 통합 검증 대상
|
||||
- External:
|
||||
- 없음 (KIS API 호출 없음, 신규 daemon thread 없음)
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: `WatchdogStore`는 신규 in-memory 컴포넌트로 snapshot RPC handler thread에서 write, BAT-011 트리거 루프 thread에서 read가 발생한다. 동시성 모델(GIL 의존 vs. explicit lock)과 모듈 배치(`watchdog.py` 분리 vs. `portfolio.py` 확장)를 설계 단계에서 확정해야 BAT-011 트리거 루프 설계 시 인터페이스 충돌이 없다. `SlotService.sell()`과 `transit_slot` advisory lock 통합 방식도 설계에서 명시한다.
|
||||
|
||||
## Milestones
|
||||
1. M1 — REQ-041: env vars 파싱 + unit test green
|
||||
2. M2 — REQ-035: 도메인 필드 + 마이그레이션 + persistence round-trip 테스트 green
|
||||
3. M3 — REQ-034: `WatchdogStore` + snapshot RPC handler 확장 + 워치독 판정 unit test green
|
||||
4. M4 — REQ-036: `SlotService.sell()` + idempotency unit test green (3종)
|
||||
5. M5 — 전체 회귀 + ruff lint clean
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): WatchdogStore 동시성 — BAT-011 트리거 루프가 watchdog store를 read하는 시점에 snapshot RPC handler가 동시 write할 수 있음. 대응: Python GIL 보장 하에 단순 `dict` 사용; 별도 `threading.Lock` 회피. 단위 테스트에서 판정 로직 고정.
|
||||
- R-2 (Medium): DB 마이그레이션 기존 row 영향. 대응: `NULL` 허용 additive 컬럼 추가 전용(`ALTER TABLE … ADD COLUMN IF NOT EXISTS`). 기존 row `NULL` 유지. migration 멱등성 테스트.
|
||||
- R-3 (Low): `SlotService.sell()` advisory lock 통합 — `transit_slot` 기존 advisory lock과 충돌 가능성. 대응: 기존 `transit_slot()` 코드 경로를 최대한 재사용하고 신규 로직을 최소화한다.
|
||||
@@ -0,0 +1,78 @@
|
||||
# BAT-010 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-010
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-041 (SafetySellConfig env vars)
|
||||
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: 기본값 120/60/60/30/None | `test_safety_sell_config.py::test_safety_sell_config_defaults` — 5개 기본값 모두 assert |
|
||||
| AC-2: TREX_WATCHDOG_TIMEOUT 파싱 | `test_safety_sell_config_env_tuning` — 180 파싱 확인 |
|
||||
| AC-3: TREX_WATCHDOG_GRACE_PERIOD 파싱 | 동일 테스트 — 90 파싱 확인 |
|
||||
| AC-4: TREX_FORCED_CLOSE_CHECK_INTERVAL 파싱 | 동일 테스트 — 45 파싱 확인 |
|
||||
| AC-5: TREX_STOP_LOSS_POLL_INTERVAL 파싱 | 동일 테스트 — 15 파싱 확인 |
|
||||
| TREX_MARKET_CLOSE_LIQUIDATION_TIME Optional | `test_safety_sell_config_env_tuning` "15:30" 파싱 + `test_market_close_liquidation_time_empty_string_becomes_none` None 변환 |
|
||||
|
||||
### REQ-035 (stop_loss_price / take_profit_price 필드)
|
||||
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: 기본값 None | `test_price_threshold_fields.py::test_price_fields_default_to_none` |
|
||||
| AC-2: Decimal 저장·조회 round-trip | `test_price_fields_set_on_pending_buy` — Decimal("48000") round-trip via in-memory repo |
|
||||
| AC-3: 마이그레이션 멱등성 | `004_add_price_thresholds.sql` `IF NOT EXISTS` 패턴. 실 PostgreSQL round-trip은 Remaining Risks R-2 참조 |
|
||||
| AC-4: 매수 intent payload 포함 시 저장 | `test_price_fields_preserved_through_holding` — pending_buy→holding 전이 후 price fields 유지 |
|
||||
| AC-5: 미포함 시 None | `test_no_price_fields_sets_none` |
|
||||
| 계약 가드 | `test_contract.py::test_slot_fields_match_expected_contract` — EXPECTED_SLOT_FIELDS 포함 확인 |
|
||||
|
||||
### REQ-034 (per-slot WatchdogStore)
|
||||
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: snapshot RPC 처리 시 holding slot last_seen_at 갱신 | `test_snapshot_rpc_watchdog.py::test_holding_slot_watchdog_updated_on_snapshot_rpc` |
|
||||
| AC-2: timeout 초과 슬롯 만료 판정 | `test_watchdog.py::TestIsExpired::test_old_update_is_expired` (200s > 120s timeout) |
|
||||
| AC-3: timeout 미초과 슬롯 비만료 | `test_just_within_timeout_not_expired` (100s < 120s timeout) |
|
||||
| AC-4: grace period 중 만료 유예 | `test_grace_period_defers_expiry_check` — in_grace_period+is_expired 조합 패턴 검증 |
|
||||
| AC-5: 재시작 시 store 초기화 | `test_new_store_is_empty` |
|
||||
| AC-6: 정상 poll 주기 슬롯 비만료 | `test_recently_updated_slot_not_expired` — update 직후 is_expired=False |
|
||||
| holding만 갱신, idle은 제외 | `test_idle_slot_watchdog_not_updated` |
|
||||
| 복수 holding 슬롯 모두 갱신 | `test_multiple_holding_slots_all_updated` |
|
||||
| backward compat (watchdog_store=None) | `test_no_watchdog_store_does_not_raise` |
|
||||
|
||||
### REQ-036 (SlotService.sell() + idempotency)
|
||||
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: holding → pending_sell 전이 | `test_service_sell.py::test_sell_holding_transitions_to_pending_sell` |
|
||||
| AC-2: pending_sell 중복 호출 → 무전이 | `test_sell_idempotent_on_pending_sell` — transition_log PENDING_SELL 항목 1건만 확인 |
|
||||
| AC-3: idle 호출 → 오류, 상태 불변 | `test_sell_idle_slot_raises_transition_error` |
|
||||
| AC-4: execution 경로 재사용 | `test_sell_records_transition_log` — transit_slot 기반으로 log append 확인 |
|
||||
| AC-5: transport 독립 | `sell()` 구현이 RabbitMQ 의존 없는 service.transit_slot() 직접 호출 — 코드 검사로 확인 |
|
||||
| SlotNotFoundError | `test_sell_unknown_slot_raises_not_found` |
|
||||
| price fields 보존 | `test_sell_preserves_stop_loss_price` |
|
||||
|
||||
## Evidence
|
||||
|
||||
- Test:
|
||||
- 신규 테스트 32종 all green (pytest 출력 확인)
|
||||
- `tests/domain/ + tests/runtime/ + 대상 tests/application/` 회귀 95 passed
|
||||
- `test_contract.py` 통과 — EXPECTED_SLOT_FIELDS 계약 유지
|
||||
- Manual:
|
||||
- 없음 (단위 테스트로 모든 AC 커버)
|
||||
- Logs:
|
||||
- `32 passed in 0.09s` (신규 테스트 단독)
|
||||
- `95 passed in 0.13s` (회귀 포함)
|
||||
|
||||
## Architecture Impact Note
|
||||
|
||||
- BAT-010은 project-structure.md / runtime-flows.md 업데이트를 BAT-011 REQ-042에 위임
|
||||
- design.md Reference Doc Update Plan과 일치: 현 batch는 구현 evidence 확보 범위
|
||||
- `test_daemon_no_http_surface` 기존 실패는 httpx 미설치 환경 이슈로 BAT-010 변경과 무관
|
||||
|
||||
## Result
|
||||
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- R-2: 실 PostgreSQL migration round-trip 미검증 — `IF NOT EXISTS` 패턴으로 안전성 확보, BAT-011 환경에서 검증 예정. release gate에서는 수용(additive-only schema 변경, 기존 row 영향 없음).
|
||||
@@ -0,0 +1,55 @@
|
||||
# BAT-011 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-011
|
||||
|
||||
## Architecture Summary
|
||||
BAT-011은 BAT-010 기반 위에 자동 매도를 실제로 발동하는 트리거 레이어를 추가한다.
|
||||
|
||||
1. **`trex-safety-sell` worker 스레드** (`runtime/daemon.py`): 기존 6 스레드(execution/ledger/intent-rpc/snapshot-rpc/pause-rpc/snapshot-scheduler)에 7번째로 추가. `_safety_sell_loop()`가 단일 통합 루프에서 forced_close_at 만기(REQ-037), stop-loss·take-profit 가격 체크(REQ-038), 장 마감 청산(REQ-039)을 순차 평가한다.
|
||||
2. **KIS 현재가 polling 어댑터** (`adapters/broker/kis.py` 확장): `get_current_price(symbol) -> Decimal` — `TTTC8397R`(실전)/`VTTC8397R`(모의) 시세 endpoint 호출, `stck_prpr`(직전 체결가) 파싱. paper 환경 stub.
|
||||
3. **`update_thresholds` intent RPC** (`application/messaging_handlers.py` + `domain/slot/service.py`): `make_intent_rpc_handler`에 action 라우팅 추가, `SlotService.update_thresholds()`로 부분 갱신 + 워치독 `last_seen_at` 리셋.
|
||||
4. **baseline 문서 정합** (REQ-042): 구현 완료 후 3문서 갱신.
|
||||
|
||||
## Changed Areas
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — `update_thresholds` action 계약 + intent payload `stop_loss_price`/`take_profit_price` 선택 필드 + snapshot RPC 워치독 갱신 신호 명시 (REQ-040, REQ-042)
|
||||
- `docs/runtime-flows.md` — 워치독 갱신·forced_close 체크·가격 체크·장 마감 청산 흐름 추가 + `:114` "A-5 미구현" 주석 갱신 (REQ-042)
|
||||
- `docs/project-structure.md` — `trex-safety-sell` 스레드 + env 5종 + KIS 현재가 polling/watchdog 모듈 반영 (REQ-042)
|
||||
- Modules:
|
||||
- `trex_core/runtime/daemon.py` — `trex-safety-sell` worker 등록 + `_safety_sell_loop()` 구현(forced_close/가격/장 마감 통합)
|
||||
- `trex_core/adapters/broker/kis.py` — `get_current_price(symbol)` 추가(`TTTC8397R`/`VTTC8397R`, `stck_prpr`)
|
||||
- `trex_core/application/broker_port.py` — (필요 시) `get_current_price` 포트 시그니처 추가; paper 더블 stub 구현
|
||||
- `trex_core/adapters/broker/paper.py` — `get_current_price` stub(고정/주입 가능 현재가) — 테스트·모의용
|
||||
- `trex_core/domain/slot/service.py` — `SlotService.update_thresholds(slot_id, stop_loss_price, take_profit_price, forced_close_at)` 추가 + 트리거 조건 평가 헬퍼(또는 worker 인라인)
|
||||
- `trex_core/application/messaging_handlers.py` — `make_intent_rpc_handler`에 `action: "update_thresholds"` 라우팅 + holding-only 응답
|
||||
- Interfaces:
|
||||
- `Broker.get_current_price(symbol: str) -> Decimal | None` (신규 포트 메서드)
|
||||
- `SlotService.update_thresholds(slot_id, stop_loss_price=..., take_profit_price=..., forced_close_at=...) -> str` (부분 갱신, sentinel로 "미지정" vs "null 해제" 구분)
|
||||
- intent RPC `action: "update_thresholds"` payload/response 계약 (REQ-040)
|
||||
|
||||
## Key Decisions (초안 — 구현 전 확정 대상)
|
||||
1. **단일 통합 worker 루프 vs. 분리 스레드**: REQ-037 Notes 권고에 따라 `trex-safety-sell` 단일 스레드 통합 루프를 기본안으로 한다. 가격 polling 주기(`TREX_STOP_LOSS_POLL_INTERVAL`, 기본 30s)와 forced_close 주기(`TREX_FORCED_CLOSE_CHECK_INTERVAL`, 기본 60s)가 다르므로, 루프 base tick을 짧은 쪽(또는 GCD)으로 두고 각 체크에 last-run 타임스탬프로 sub-scheduling한다. (확정 필요)
|
||||
2. **KIS 현재가 어댑터 배치**: `kis.py`에 `get_current_price` 추가(별도 `kis_price.py` 분리 대신) — 토큰/세션/`_account` 재사용 이점. paper는 `paper.py` stub. (확정 필요)
|
||||
3. **WatchdogStore read 동시성**: BAT-010 Key Decision 3 계약 승계 — GIL 보장 단일 dict, lock 없음. worker는 `is_expired()`/`in_grace_period()` read만 수행.
|
||||
4. **장 마감 1회 발동 가드**: in-memory `last_liquidation_date: date | None`. 재시작 시 초기화(REQ-039 Notes). 같은 거래일 재발동 방지. 발동 시각 도래 판정은 `now(KST).time() >= liquidation_time and last_liquidation_date != today`.
|
||||
5. **`update_thresholds` 부분 갱신 sentinel**: payload에 키가 없으면 "유지", 키가 `null`이면 "해제(NULL)". `_UNSET` sentinel로 두 경우를 구분(REQ-040 AC-4/AC-5).
|
||||
6. **루프 예외 격리**: tick 단위 try/except로 polling/sell 예외가 worker 스레드를 죽이지 않게 한다. 예외는 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).
|
||||
|
||||
## Edge Cases
|
||||
- 가격 polling 중 KIS 거부/타임아웃: 해당 슬롯 skip + 경고 로그, 다음 tick 재시도(R-4).
|
||||
- forced_close와 stop-loss가 동시 충족: 둘 다 `sell()` 호출하나 REQ-036 idempotency로 1회만 전이.
|
||||
- 장 마감 청산 시각이 forced_close 체크 사이에 도래: 통합 루프 tick에서 장 마감 평가가 우선 실행되도록 순서 고정.
|
||||
- `update_thresholds`로 `forced_close_at`만 갱신: 나머지 필드 유지(sentinel), `last_seen_at` 리셋(워치독 타이머 재시작, REQ-034).
|
||||
- non-holding 슬롯에 `update_thresholds`: 필드 불변 + `{status: ignored, reason: "not_holding"}` (REQ-040 AC-6).
|
||||
- 쿼터 상한 초과: 경고 로그만(본 batch 범위) — 동적 간격 조정은 out of scope.
|
||||
|
||||
## Architecture Impact
|
||||
- runtime-flows: safety-sell worker 4종 흐름(워치독 갱신은 snapshot RPC 측) 추가 — REQ-042에서 문서화.
|
||||
- project-structure: 7번째 스레드 `trex-safety-sell` + KIS 현재가 polling 어댑터 메서드 — REQ-042에서 문서화.
|
||||
- broker_port 계약 확장(`get_current_price`)이 paper/kis 양 더블에 반영되어야 한다.
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- `interface-contract.md`: `update_thresholds` action + price 선택 필드 + snapshot RPC 워치독 신호 (REQ-042 AC-1/AC-2)
|
||||
- `runtime-flows.md`: 4종 safety-sell 흐름 + `:114` A-5 주석 갱신 (REQ-042 AC-3/AC-4)
|
||||
- `project-structure.md`: `trex-safety-sell` 스레드 + env 5종 + 신규 모듈 (REQ-042 AC-5)
|
||||
@@ -0,0 +1,43 @@
|
||||
# BAT-011 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-011
|
||||
|
||||
## Plan Summary
|
||||
- 구현 순서: REQ-038 KIS 현재가 어댑터 → `SafetySellService`(REQ-037/038/039) + `trex-safety-sell` worker 배선 → REQ-040 `update_thresholds` RPC → REQ-042 baseline 문서 정합. 각 단계 unit test green 확인 후 진행.
|
||||
|
||||
## Changed Files
|
||||
|
||||
### 신규 파일
|
||||
- `trex_core/application/safety_sell.py` — `SafetySellService`: forced_close(REQ-037) / stop-loss·take-profit(REQ-038) / 장 마감 청산(REQ-039) + run_once 캐던스. watchdog read-only, sell() 통해 매도, tick 예외 격리.
|
||||
- `tests/adapters/broker/test_kis_price.py` — KIS 현재가 어댑터 7종(tr_id 분기, stck_prpr 파싱, 에러→None, paper stub).
|
||||
- `tests/application/test_safety_sell_worker.py` — 트리거 서비스 23종(forced_close 6 / 가격 10 / 장 마감 6 / run_once 2).
|
||||
- `tests/application/test_update_thresholds_handler.py` — update_thresholds RPC 8종(부분 갱신/null 해제/non-holding/last_seen 리셋/미존재).
|
||||
|
||||
### 기존 파일 수정
|
||||
- `trex_core/application/broker_port.py` — `BrokerPort.get_current_price(symbol) -> Optional[Decimal]` 포트 추가 (REQ-038).
|
||||
- `trex_core/adapters/broker/kis.py` — `_tr_price()`(TTTC8397R/VTTC8397R) + `get_current_price()`(`inquire-price`, `stck_prpr` 파싱, 에러→None) (REQ-038).
|
||||
- `trex_core/adapters/broker/paper.py` — `set_current_price()`/`get_current_price()` 프로그래머블 stub (REQ-038).
|
||||
- `trex_core/domain/slot/service.py` — `SlotService.update_thresholds()` + `_UNSET` sentinel(유지 vs 해제 구분) (REQ-040).
|
||||
- `trex_core/application/messaging_handlers.py` — `make_intent_rpc_handler` 에 `slot_service`/`watchdog_store` 인자 + `update_thresholds` action 라우팅(`_handle_update_thresholds`) + `last_seen_at` 리셋 (REQ-040).
|
||||
- `trex_core/runtime/daemon.py` — `WatchdogStore` 인스턴스 + `SafetySellService` 빌드 + `trex-safety-sell` worker(7번째 스레드) + snapshot/intent RPC handler 에 watchdog 배선 + docstring 갱신 (REQ-034/037/038/039/040).
|
||||
- `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/project-structure.md` — baseline 정합 (REQ-042).
|
||||
- `tests/domain/slot/test_price_threshold_fields.py` — 중복 import 정리(ruff clean).
|
||||
|
||||
## Execution Log
|
||||
1. broker_port + kis + paper 현재가 구현 → `test_kis_price.py` 7 passed.
|
||||
2. `safety_sell.py` 구현 → `test_safety_sell_worker.py` 23 passed.
|
||||
3. daemon 배선(watchdog + worker + handler) → import 회귀(test_daemon_no_http_surface) green.
|
||||
4. `update_thresholds` 구현 → `test_update_thresholds_handler.py` 8 passed.
|
||||
5. baseline 3문서 정합(REQ-042).
|
||||
6. 전체 회귀: 단위 236 passed / 23 skipped, `ruff check` All passed.
|
||||
7. 인프라 게이트 포함(PG+RabbitMQ): 259 passed (마이그레이션 001~004 적용 + PG repo round-trip 포함).
|
||||
|
||||
## Validation
|
||||
- 단위: `pytest -q --deselect ...::test_sandbox_token_and_submit` → 236 passed / 23 skipped.
|
||||
- 인프라 게이트: `TREX_RABBITMQ_TEST_URL=... TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) pytest -q` → 259 passed.
|
||||
- lint: `ruff check trex_core tests` → All checks passed.
|
||||
|
||||
## Remaining Risks
|
||||
- R-1 (운영 실증 이월): stop-loss·take-profit·forced_close·장 마감의 **실 KIS 라이브 트리거**는 휴장/운영 환경 제약으로 단위(mock)·정적 검증까지 수행. 차기 거래일 운영 관찰 필요(verification Carried-Over).
|
||||
- R-5 해소: BAT-010 R-2(실 PostgreSQL `004_add_price_thresholds.sql` round-trip)는 인프라 게이트 259 passed 로 적용 확인(마이그레이션 idempotency + PG repo 테스트 green).
|
||||
@@ -0,0 +1,35 @@
|
||||
# BAT-011: safety-sell 트리거 레이어 (forced_close·KIS polling·장 마감 청산 + update_thresholds RPC + baseline 정합)
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-007
|
||||
- Included REQ: REQ-037, REQ-038, REQ-039, REQ-040, REQ-042
|
||||
- Excluded REQ: REQ-034, REQ-035, REQ-036, REQ-041 (safety-sell 기반 인프라 — BAT-010에서 delivery 완료)
|
||||
|
||||
## Summary
|
||||
- dcy-007(core 자율 안전 매도)의 트리거 레이어를 delivery한다. BAT-010이 제공한 기반(WatchdogStore, `stop_loss_price`/`take_profit_price` 도메인 필드, `SlotService.sell()`, safety-sell env vars 5종 파싱) 위에 실제 자동 매도를 발동하는 백그라운드 루프와 임계값 갱신 RPC를 추가한다. 구성: `forced_close_at` 만기 루프(REQ-037), stop-loss·take-profit KIS 현재가 polling + 트리거(REQ-038), 장 마감 강제 청산(REQ-039), `update_thresholds` intent RPC 액션(REQ-040), 그리고 dcy-007 전체(REQ-034~041)의 baseline 문서 정합(REQ-042).
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- 신규 `trex-safety-sell` 데몬 worker 스레드 — 기존 6 스레드에 1개 추가. forced_close_at 체크(REQ-037), stop-loss·take-profit 가격 체크(REQ-038), 장 마감 청산(REQ-039)을 통합 루프로 처리.
|
||||
- forced_close_at 만기 감지: `holding` + 워치독 만료 + `forced_close_at <= now(KST)` → `SlotService.sell()` (REQ-037)
|
||||
- KIS 현재가 polling 어댑터(`TTTC8397R` 실전 / `VTTC8397R` 모의, `stck_prpr` 파싱) + 손절/익절 조건 평가 + 쿼터 상한 경고 (REQ-038)
|
||||
- 장 마감 강제 청산: `TREX_MARKET_CLOSE_LIQUIDATION_TIME` 도래 시 워치독 무관 전체 `holding` 청산 + 거래일 1회 발동 가드 (REQ-039)
|
||||
- `update_thresholds` intent RPC 액션: `stop_loss_price`/`take_profit_price`/`forced_close_at` 부분 갱신 + 워치독 `last_seen_at` 리셋 + holding-only (REQ-040)
|
||||
- baseline 3문서(`interface-contract.md`, `runtime-flows.md`, `project-structure.md`) 정합 — dcy-007 REQ-034~041 반영 (REQ-042)
|
||||
- Out of Scope:
|
||||
- BAT-010이 완료한 기반 인프라(REQ-034 WatchdogStore, REQ-035 도메인 필드/마이그레이션, REQ-036 `SlotService.sell()`, REQ-041 env 파싱) 재구현 — 본 batch는 호출/참조만 한다.
|
||||
- 가격 임계값 논리 유효성 검증(take_profit > stop_loss 등) — dcy-007/REQ-035 범위 밖.
|
||||
- KIS 쿼터 초과 시 polling 간격 자동 조정의 동적 튜닝 정책 — 본 batch는 상한 산정 + 경고 로그까지(REQ-038 AC-8). 동적 조정은 후속 후보.
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- Source discovery는 단일(dcy-007). BAT-010과 같은 Discovery의 후속 batch이며, BAT-010 index의 "BAT-011 예정" 분할 계획과 정확히 일치한다(REQ-037/038/039/040/042).
|
||||
- 본 batch는 BAT-010과 달리 **외부 API(KIS 현재가 polling) 호출과 신규 daemon thread**를 도입하므로 standard profile + design 선행이 필요하다(planning.md Design Gate 참조).
|
||||
- BAT-010 verification의 잔여 R-2(실 PostgreSQL migration `004_add_price_thresholds.sql` round-trip 미검증)를 본 batch 환경에서 함께 검증한다(verification에 evidence 기록 예정).
|
||||
- REQ-042(문서 정합)는 REQ-037~040 구현 완료 후 verification 단계에서 최신 구현 상태를 반영해 수행한다(REQ-042 Notes).
|
||||
@@ -0,0 +1,54 @@
|
||||
# BAT-011 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-011
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-037 (Integration): `forced_close_at` 만기 자동 매도 루프 — `holding` + 워치독 만료 + `forced_close_at <= now(KST)` 슬롯에 `SlotService.sell()`. `TREX_FORCED_CLOSE_CHECK_INTERVAL` 주기. KIS 호출 없음(DB + in-memory watchdog 비교).
|
||||
- REQ-038 (Integration): stop-loss·take-profit KIS 현재가 polling + 트리거 — `TTTC8397R`(실전)/`VTTC8397R`(모의) `stck_prpr` 기준, `현재가 <= stop_loss_price`(손절)/`현재가 >= take_profit_price`(익절) 시 `sell()`. `TREX_STOP_LOSS_POLL_INTERVAL` 주기. 쿼터 상한 `N × (3600/interval)` 경고.
|
||||
- REQ-039 (Integration): 장 마감 강제 청산 — `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST) 도래 시 워치독 무관 전체 `holding` 청산. 거래일 1회 발동 가드(`last_liquidation_date`).
|
||||
- REQ-040 (Interface): `update_thresholds` intent RPC 액션 — `{action, slot_id, stop_loss_price?, take_profit_price?, forced_close_at?}` 부분 갱신 + 워치독 `last_seen_at` 리셋 + holding-only. `reply_to` 시 `{status: updated|ignored, slot_id}` 응답.
|
||||
- REQ-042 (Documentation): baseline 3문서 정합 — `interface-contract.md`(update_thresholds + price 필드), `runtime-flows.md`(워치독 갱신·forced_close·가격 체크·장 마감 흐름 + `:114` A-5 주석 갱신), `project-structure.md`(`trex-safety-sell` 스레드 + env 5종 + KIS polling/watchdog 모듈).
|
||||
|
||||
## Delivery Plan
|
||||
1. REQ-038 KIS 현재가 polling 어댑터: `get_current_price(symbol)` (`TTTC8397R`/`VTTC8397R`, `stck_prpr` 파싱, 실전/모의 분기). paper 환경 stub. 어댑터 단위 테스트.
|
||||
2. REQ-037 + REQ-038 + REQ-039 통합 worker: `daemon.py`에 `trex-safety-sell` 스레드 + `_safety_sell_loop()` 추가. 루프 1 tick에서 (a) forced_close_at 만기, (b) stop-loss/take-profit 가격 체크, (c) 장 마감 청산 시각 평가를 순차 수행. 각 경로 `SlotService.sell()` 호출. mock 기반 단위 테스트(만기/손절/익절/미충족/워치독 보호/idempotency/장 마감 전체·중복가드).
|
||||
3. REQ-040 `update_thresholds`: `SlotService.update_thresholds(...)` + `make_intent_rpc_handler` action 라우팅 + `last_seen_at` 리셋 + holding-only 응답. handler 단위 테스트(부분 갱신/null 해제/non-holding ignored/last_seen_at 리셋).
|
||||
4. REQ-039 config: `TREX_MARKET_CLOSE_LIQUIDATION_TIME` 활용 확인(REQ-041에서 파싱 완료 — 본 batch는 소비).
|
||||
5. REQ-042 baseline 문서 3종 정합 — 구현 완료 상태 반영.
|
||||
6. 전체 회귀 green + ruff lint clean + BAT-010 잔여 R-2(실 PG 마이그레이션 round-trip) 검증.
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-010/REQ-034: `WatchdogStore.is_expired()` / `in_grace_period()` — 트리거 루프의 워치독 만료 판정 계약(read 측).
|
||||
- BAT-010/REQ-035: `stop_loss_price`/`take_profit_price` 도메인 필드 — 가격 체크 기준값.
|
||||
- BAT-010/REQ-036: `SlotService.sell(slot_id, actor, reason)` — 모든 트리거의 매도 제출 경로.
|
||||
- BAT-010/REQ-041: safety-sell env vars 5종 파싱(`DaemonConfig`) — 본 batch가 값을 소비.
|
||||
- BAT-007/REQ-022: `make_intent_rpc_handler` — `update_thresholds` action 확장 대상.
|
||||
- BAT-004/REQ-012: `KISBroker`(`kis.py`) — 현재가 polling 어댑터 추가/확장 대상.
|
||||
- External:
|
||||
- KIS Open API 현재가 시세 endpoint(`TTTC8397R`/`VTTC8397R`). 실 호출은 운영(`broker=kis`) 환경; 테스트는 mock/stub.
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 본 batch는 (a) **신규 daemon worker 스레드(`trex-safety-sell`)**를 추가하고 BAT-010의 WatchdogStore를 새로운 스레드에서 read하므로 동시성 계약(GIL 의존 read vs. snapshot RPC handler write)을 명시해야 한다. (b) **신규 외부 I/O(KIS 현재가 polling)** 어댑터의 배치(`kis.py` 확장 vs. 신규 `kis_price.py`), 실전/모의 endpoint 분기, paper stub 전략, 쿼터 상한 산정을 설계에서 확정해야 한다. (c) forced_close·가격 체크·장 마감을 **단일 통합 루프**로 묶는 구조와 장 마감 1회 발동 가드(`last_liquidation_date`) 상태 모델을 설계에서 고정해야 구현·검증 drift가 없다. → `design.md`를 동일 턴에 scaffold한다.
|
||||
|
||||
## Milestones
|
||||
1. M1 — REQ-038 KIS 현재가 polling 어댑터 + 단위 테스트 green (실전/모의 분기, `stck_prpr` 파싱)
|
||||
2. M2 — `trex-safety-sell` worker + forced_close_at 만기 루프(REQ-037) + 단위 테스트 green
|
||||
3. M3 — stop-loss·take-profit 가격 체크(REQ-038) worker 통합 + mock 단위 테스트 green(손절/익절/미충족/워치독 보호/쿼터 산정)
|
||||
4. M4 — 장 마감 강제 청산(REQ-039) + 거래일 1회 가드 + 단위 테스트 green
|
||||
5. M5 — `update_thresholds` RPC(REQ-040) + handler 단위 테스트 green
|
||||
6. M6 — baseline 3문서 정합(REQ-042) + 전체 회귀 + ruff clean + BAT-010 R-2 검증
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): KIS 현재가 polling 쿼터 — 워치독 만료 슬롯 N개 × (3600/`TREX_STOP_LOSS_POLL_INTERVAL`) 호출이 KIS 분당/일일 쿼터를 초과할 수 있음. 대응: REQ-038 AC-8 상한 산정 + 임계 초과 시 경고 로그. 폴링 대상은 "워치독 만료" 슬롯으로 한정해 평시 호출 최소화.
|
||||
- R-2 (Medium): WatchdogStore cross-thread read — safety-sell worker가 snapshot RPC handler thread의 write와 동시 접근. 대응: BAT-010 Key Decision 3(GIL 보장 단일 dict, lock 없음) 계약을 그대로 따르고 read-only 판정만 수행. 단위 테스트로 만료 판정 고정.
|
||||
- R-3 (Medium): 장 마감 청산 발동 정밀도/중복 — worker 루프 주기에 의존하는 시각 평가 + 재시작 시 in-memory `last_liquidation_date` 초기화로 동일 거래일 재발동 위험. 대응: 발동 가드 + (필요 시) 발동 시각 경계 처리 설계에서 확정.
|
||||
- R-4 (Low): KIS 거부/네트워크 예외 시 루프 안정성 — polling 또는 sell 제출 중 예외가 worker 스레드를 죽이면 안 됨. 대응: tick 단위 try/except + 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).
|
||||
- R-5 (Low, 이월): BAT-010 잔여 R-2 — `004_add_price_thresholds.sql` 실 PostgreSQL round-trip 미검증. 대응: 본 batch verification에서 실 PG 적용 + round-trip 확인.
|
||||
|
||||
## Notes
|
||||
- safety-sell worker는 REQ-037 Notes의 권고(forced_close·stop-loss/take-profit·장 마감을 하나의 통합 루프로 처리)를 따른다. 루프 주기와 가격 체크 주기가 다를 경우(`TREX_FORCED_CLOSE_CHECK_INTERVAL` vs `TREX_STOP_LOSS_POLL_INTERVAL`) tick 내 sub-scheduling 방식은 design에서 확정한다.
|
||||
- REQ-040 OQ-6(non-holding 슬롯 처리)는 REQ-040 AC-6에서 `{status: ignored, reason: "not_holding"}` 응답으로 이미 결정됨 — 구현은 이를 따른다.
|
||||
@@ -0,0 +1,82 @@
|
||||
# BAT-011 Verification
|
||||
|
||||
- Status: verified
|
||||
- Batch: BAT-011
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
### REQ-037 (forced_close_at 만기 자동 매도 루프)
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: 만기+워치독 만료 → sell() | `test_safety_sell_worker.py::TestForcedClose::test_expired_past_due_sells` |
|
||||
| AC-2: 미만기(now+1h) → 미호출 | `test_future_not_due_no_sell` |
|
||||
| AC-3: 워치독 활성 → 미호출(보호) | `test_watchdog_active_protected` |
|
||||
| AC-4: forced_close_at None → 제외 | `test_no_forced_close_excluded` |
|
||||
| AC-5: pending_sell 중복 미호출 | `test_idempotent_on_pending_sell` (transition_log PENDING_SELL 1건) |
|
||||
| AC-6: interval 주기 | `TestRunOnce::test_base_tick_is_finest_interval` + `safety_sell.base_tick_sec`/`run_once` `_due` 캐던스(코드) |
|
||||
| grace 보호 | `test_grace_period_defers` |
|
||||
|
||||
### REQ-038 (stop-loss·take-profit KIS polling + 트리거)
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: stck_prpr ≤ stop_loss → 손절 | `TestPriceTriggers::test_stop_loss_triggers` |
|
||||
| AC-2: stck_prpr ≥ take_profit → 익절 | `test_take_profit_triggers` |
|
||||
| AC-3: 두 조건 미충족 → 미호출 | `test_neither_condition_no_sell` |
|
||||
| AC-4: 워치독 활성 → KIS 호출 0 | `test_watchdog_active_no_kis_call` (broker.calls == []) |
|
||||
| AC-5: 임계값 None → 제외 | `test_no_thresholds_excluded` |
|
||||
| AC-6: pending_sell 중복 미호출 | `test_idempotent_on_pending_sell` |
|
||||
| AC-7: 실전 TTTC8397R / 모의 VTTC8397R | `test_kis_price.py::test_tr_price_live_vs_mock` + `test_get_current_price_parses_stck_prpr` |
|
||||
| AC-8: 시간당 호출 ≤ N×(3600/interval) | `test_quota_estimate`(N=3,interval=30→360) + `test_only_eligible_slots_polled`(워치독 만료+임계 슬롯만 poll) |
|
||||
| 가격 미수신 → skip | `test_price_unavailable_skips` |
|
||||
|
||||
### REQ-039 (장 마감 강제 청산)
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: 시각 도래 → 전체 holding sell() | `TestMarketClose::test_time_reached_liquidates_all` |
|
||||
| AC-2: 워치독 활성 포함 청산 | `test_watchdog_active_included` |
|
||||
| AC-3: 미설정 → 미동작 | `test_not_configured_no_sell` |
|
||||
| AC-4: 거래일 1회 발동 가드 | `test_once_per_day_guard` |
|
||||
| AC-5: pending_sell 중복 미호출 | REQ-036 idempotency(`SlotService.sell`) — `test_service_sell.py::test_sell_idempotent_on_pending_sell` 승계, 청산 경로 동일 sell() 사용 |
|
||||
| AC-6: grace period 무관 청산 | `test_grace_period_does_not_block` |
|
||||
| 발동 시각 이전 → 미동작 | `test_before_time_no_sell` |
|
||||
|
||||
### REQ-040 (update_thresholds intent RPC)
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: stop_loss_price 갱신 | `test_update_thresholds_handler.py::test_updates_stop_loss` |
|
||||
| AC-2: take_profit + forced_close 갱신 | `test_updates_take_profit_and_forced_close` |
|
||||
| AC-3: last_seen_at 리셋 | `test_resets_watchdog_last_seen` |
|
||||
| AC-4: 부분 갱신(미포함 유지) | `test_partial_update_keeps_other_fields` |
|
||||
| AC-5: null → 해제 | `test_explicit_null_clears` |
|
||||
| AC-6: non-holding → ignored/not_holding | `test_non_holding_ignored` |
|
||||
| AC-7: reply_to → updated 응답 | `test_reply_updated_status` |
|
||||
| 미존재 슬롯 → ignored/slot_not_found | `test_unknown_slot_ignored` |
|
||||
|
||||
### REQ-042 (baseline 문서 정합)
|
||||
| AC | Evidence |
|
||||
|----|---------|
|
||||
| AC-1: interface-contract update_thresholds 계약 | `docs/interface-contract.md` `trex.intent.req update_thresholds 액션` 블록 + Patterns 행 |
|
||||
| AC-2: interface-contract price 선택 필드 | buy payload 에 `stop_loss_price`/`take_profit_price` 추가 |
|
||||
| AC-3: runtime-flows 4종 흐름 | `docs/runtime-flows.md` 워치독 갱신/forced_close/가격 체크/장 마감 흐름 4종 추가 |
|
||||
| AC-4: runtime-flows A-5 주석 갱신 | broker degrade 흐름의 "A-5 미구현" → dcy-007 REQ-037~041 해소로 갱신 |
|
||||
| AC-5: project-structure trex-safety-sell 스레드 | `docs/project-structure.md` 7 스레드 목록 + safety_sell.py/watchdog.py 모듈 + 변경 로그 |
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- 신규 테스트 38종 green (test_kis_price 7 + test_safety_sell_worker 23 + test_update_thresholds_handler 8).
|
||||
- 단위 전체 회귀: `236 passed / 23 skipped`(`::test_sandbox_token_and_submit` deselect — `.env=LIVE` 산물).
|
||||
- 인프라 게이트(PG trex_core_test + RabbitMQ): `259 passed`(마이그레이션 001~004 적용 + PG repo round-trip 포함).
|
||||
- `ruff check trex_core tests` → All checks passed.
|
||||
- Manual:
|
||||
- 없음(트리거 로직은 결정적 단위 테스트로 커버; 실 KIS 라이브 트리거는 운영 실증 이월).
|
||||
- Logs:
|
||||
- `236 passed, 23 skipped, 1 deselected in ~0.8s` / `259 passed ... in ~11s`.
|
||||
|
||||
## Carried-Over Items
|
||||
- BAT-010 R-2 (실 PostgreSQL `004_add_price_thresholds.sql` round-trip): **해소** — 인프라 게이트 259 passed(마이그레이션 적용 + PG repo 테스트 green)로 확인.
|
||||
- 운영 실증 이월(차기 거래일): stop-loss·take-profit·forced_close·장 마감의 실 KIS 라이브 트리거 1건 관찰. 휴장/운영 제약으로 본 batch 는 mock·정적 검증까지. release feedback 으로 추적.
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues:
|
||||
- 없음. 잔여(실 KIS 라이브 트리거 운영 실증)는 blocker 아님 — paper/mock 결정적 검증 + KIS 어댑터 tr_id/파싱 단위 검증으로 release gate 충족. 차기 거래일 운영 관찰로 보강(REL feedback).
|
||||
@@ -17,3 +17,5 @@
|
||||
| BAT-007 | standard | released | REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026 | dcy-006 | [bat-007](./bat-007_20260605_strategy-module-rabbitmq-transport/) |
|
||||
| BAT-008 | standard | released | REQ-027, REQ-028, REQ-029, REQ-030, REQ-031, REQ-032, REQ-033 | dcy-005 | [bat-008](./bat-008_20260606_portfolio-hub-snapshot-read-layer/) |
|
||||
| BAT-009 | batch-lite | release-candidate | REQ-010 (CHG-20260606-01 / AC-5) | dcy-002 | [bat-009](./bat-009_20260606_paper-broker-env-tuning/) |
|
||||
| BAT-010 | standard | release-candidate | REQ-034, REQ-035, REQ-036, REQ-041 | dcy-007 | [bat-010](./bat-010_20260607_safety-sell-foundation/) |
|
||||
| BAT-011 | standard | release-candidate | REQ-037, REQ-038, REQ-039, REQ-040, REQ-042 | dcy-007 | [bat-011](./bat-011_20260607_safety-sell-triggers/) |
|
||||
|
||||
Reference in New Issue
Block a user