전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태가 무성이라 운영자가 인지할 수 없던 문제(issue #12)를 가시화한다. 거버넌스: 전부 change-req (신규 REQ 없음, dcy-017 OQ-3; Redis 미채택). - REQ-034/021: WatchdogStore.last_seen + snapshot RPC 응답에 holding 슬롯 워치독 상태(expired/last_seen) read-only 동봉(best-effort) - REQ-032: trex-core slots 가 expired 슬롯을 WATCHDOG-OWNED(전략 이탈) 표시 - REQ-037/038/039: safety-sell 워치독 소유 진입(WARN)/이탈(INFO) + 발동 (forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그 + in-process 카운터(SafetySellService.counters), 구조화 필드(slot_id/ portfolio_id/reason/phase/instance_key). per-tick 스킵은 카운터만 - REQ-042: runtime-flows/interface-contract 정합 best-effort 격리(NFR-1): 모든 가시화는 예외 격리 — 워치독 판정·청산 불변. dcy-016 억제 읽기로 관측이 워치독을 리셋하지 않음(비침습). 검증: 단위 335 passed/34 skipped, 인프라 게이트(PG+RabbitMQ) 369 passed, ruff clean. 신규 test_safety_sell_visibility(6) + snapshot watchdog(1) + CLI(1). BAT-021 rc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.1 KiB
7.1 KiB
req-037: forced_close_at 만기 자동 매도 루프
- Status: Implemented
- Type: Integration
- Priority: High
- Owner: project maintainers
Intent
슬롯에 설정된 forced_close_at 만기 시각이 도달했을 때 데몬이 자율적으로 매도를 제출하는 백그라운드 루프를 추가한다. 워치독이 만료된 슬롯에 한해 동작하므로 전략 모듈이 정상 활성 중인 슬롯에는 개입하지 않는다(dcy-007 FR-1, NFR-1).
Requirement
- 데몬은
TREX_FORCED_CLOSE_CHECK_INTERVAL(초)주기로holding상태이며 워치독이 만료된 슬롯 중forced_close_at <= now(KST)인 슬롯을 감지해야 한다. - 조건에 해당하는 슬롯에 대해
SlotService.sell()(REQ-036)을 통해 내부 매도를 제출해야 한다. - 이 루프는 daemon.py의 safety-sell worker 스레드에서 실행된다. 기존 6개 스레드에 safety-sell worker(
trex-safety-sell)를 추가한다. forced_close_at이None인 슬롯은 이 루프의 대상이 아니다.- 워치독 활성(만료되지 않은) 슬롯은 이 루프의 대상이 아니다.
Acceptance Criteria
- Given
holding상태이고 워치독 만료이며forced_close_at = now() − 1s인 슬롯 S가 있을 때, When checker 1회가 수행되면, Then S에 대해SlotService.sell()이 호출된다. - Given
holding상태이고 워치독 만료이며forced_close_at = now() + 1hour인 슬롯 S가 있을 때, When checker 1회가 수행되면, Then S에 대해sell()이 호출되지 않는다. - Given
holding상태이고 워치독 활성이며forced_close_at = now() − 1s인 슬롯 S가 있을 때, When checker 1회가 수행되면, Then S에 대해sell()이 호출되지 않는다(워치독 보호). - Given
forced_close_at = None인 슬롯 S가 있을 때, When checker 1회가 수행되면, Then S는 만기 체크 대상에서 제외된다. - Given 대상 슬롯이 이미
pending_sell일 때, When checker 1회가 수행되면, Then 중복sell()호출이 발생하지 않는다(REQ-036 idempotency 적용). - Given
TREX_FORCED_CLOSE_CHECK_INTERVAL=60일 때, When daemon이 정상 실행 중이면, Then checker는 60±허용오차 초 주기로 반복 실행된다(NFR-1: 감지 지연 ≤ 60초).
Impacted Area
- Docs:
docs/runtime-flows.md— forced_close_at 만기 감지 루프 흐름 추가 (REQ-042 참조)docs/project-structure.md—trex-safety-sell스레드 추가 반영 (REQ-042 참조)
- Modules:
trex_core/runtime/daemon.py—trex-safety-sellworker 스레드 추가,_safety_sell_loop()메서드 구현trex_core/domain/slot/service.py— forced_close_at 조건 평가 메서드(또는 checker 내 인라인)
- Tests:
- forced_close_at checker 단위 테스트 — 만기 슬롯 감지 + sell() 호출 확인
- 워치독 활성 슬롯 통과 확인
- idempotency 확인 (pending_sell 슬롯 중복 처리 없음)
Notes
- safety-sell worker(
trex-safety-sell)는 forced_close_at checker(REQ-037), stop-loss·take-profit checker(REQ-038), 장 마감 청산(REQ-039)을 하나의 통합 루프로 묶어 처리할 수 있다. 구체적인 스레드 분리/통합 방식은 batch planning에서 결정한다. - 체크 로직은 DB 조회(forced_close_at 필드)와 in-memory 워치독 store 비교만 수행하므로 KIS API 호출이 없다. 낮은 연산 비용.
TREX_FORCED_CLOSE_CHECK_INTERVALenv var는 REQ-041 참조.SlotService.sell()구현은 REQ-036 참조.
Change Log
CHG-20260607-01
- Date: 2026-06-07
- Author: Justin Ko
Change Summary
- dcy-007 confirmed → REQ-037 초안 생성
Intent
- forced_close_at 만기 자동 매도 루프(FR-1)를 독립 Integration REQ로 정규화
Acceptance Criteria Delta
- Added: AC-1~6 전체
Impacted Area Delta
- Docs: runtime-flows.md, project-structure.md (REQ-042에서 처리)
- Modules: daemon.py (스레드 추가), service.py (forced_close 조건 평가)
- Tests: checker 단위 테스트 3종
Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: suggest-batch-reqs 후 batch 배정
CHG-20260607-02
- Date: 2026-06-07
- Author: Justin Ko
Change Summary
- Proposed → Approved (confirm-req)
Intent
- 승인 게이트 통과: 필수 필드·AC·Impacted Area·Change Log 완비 확인. dcy-007(confirmed) 범위 및 결정 사항과 모순 없음.
Acceptance Criteria Delta
- (변경 없음)
Impacted Area Delta
- (변경 없음)
Delivery Trace Delta
- Batches: none (미할당)
- Releases: none
Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: suggest-batch-reqs 후 batch 배정
CHG-20260607-03
- Date: 2026-06-07
- Author: Justin Ko
Change Summary
- Status Approved → Implemented (confirm-req-implemented, 근거 batch BAT-011 release-candidate).
Intent
- BAT-011 verification.md 의 AC↔evidence(SafetySellService forced_close checker — 만기/미만기/워치독 보호/None 제외/idempotency/캐던스)로 AC-1~6 충족 → 구현 완료 동기화.
Acceptance Criteria Delta
- (변경 없음)
Impacted Area Delta
- (변경 없음)
Delivery Trace Delta
- Batches: BAT-011 (release-candidate)
- Releases: none
Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current (Implemented)
- Follow-up Action: draft-release 로 BAT-011 배포 편성.
CHG-20260608-01
- Date: 2026-06-08
- Author: Justin Ko
Change Summary
- (change-req, dcy-017/issue #12) safety-sell 가시화: 워치독 소유 진입(전략 이탈)=WARN/이탈(복귀)=INFO(
_update_ownership, run_once 매 tick), forced_close 발동=WARN(_log_fire) + in-process 카운터(SafetySellService.counters). 구조화 필드(slot_id/portfolio_id/reason/phase/instance_key). best-effort(가시화 실패가 청산을 막지 않음). 트리거 판정 동작 불변. BAT-021 재구현·재검증 완료 → Implemented 유지.
Intent
- 전략 이탈→안전망 소유, forced_close 발동을 무성에서 가시화(OQ-2 차등 매트릭스).
Acceptance Criteria Delta
- Added:
- 워치독 소유 진입/이탈 + forced_close 발동 로그/카운터. per-tick 스킵은 카운터만.
Impacted Area Delta
- Modules:
trex_core/application/safety_sell.py(_update_ownership/_log_fire/counters/instance_key),trex_core/runtime/daemon.py(instance_key 배선) - Tests: tests/application/test_safety_sell_visibility.py
Delivery Trace Delta
- Batches: BAT-021
- Releases: none
Revalidation Impact
- Reverification Needed: yes (BAT-021 에서 완료)
- Existing Implementation Invalidated: no (관측만 추가, 트리거 불변)
- Status Recommendation: Implemented (change-req → BAT-021 재검증)
- Follow-up Action: confirm-req-implemented(BAT-021).
Validation Plan
- BAT-021: 소유 진입 WARN/이탈 INFO/forced_close WARN + 카운터 단위(caplog).