feat(cli): enter --forced-close-at 시간 기반 자동청산 옵션 (issue #17, BAT-024)

trex-core enter 에 --forced-close-at <ISO8601>(절대시간) 옵션 추가.
지정 시 buy intent payload 의 기존 forced_close_at 에 매핑(계약·도메인·
forced-close 루프 REQ-037 불변, CLI 노출 갭만 보완). 잘못된 형식은 제출
전 거부, 미지정 시 미포함(하위호환). 상대시간 옵션은 미채택.

REQ-032 change-req(CHG-20260609-01/02) → BAT-024(batch-lite) →
Implemented. 단위 353 passed, ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 13:10:21 +09:00
co-authored by Claude Opus 4.8
parent fec9411d33
commit 1cbbb89091
10 changed files with 275 additions and 5 deletions
@@ -0,0 +1,32 @@
# BAT-024 Implementation
- Status: completed
- Batch: BAT-024
## Plan Summary
- 운영자 CLI `enter` 에 `--forced-close-at <ISO8601>`(절대시간) 옵션을 추가하고, 지정 시 buy intent payload 의 기존 `forced_close_at` 필드에 매핑한다(REQ-032 AC-5/6). 잘못된 형식은 제출 전 거부, 미지정 시 payload 미포함(하위호환). 도메인/계약/forced-close 루프는 불변.
## Changed Files
- `trex_core/runtime/cli.py`
- `enter` 서브파서에 `--forced-close-at`(dest `forced_close_at`) 인자 추가.
- `_cmd_enter`: 제출 **전** `datetime.fromisoformat` 으로 검증(실패 → `_fail` + rc=1), 정규화된 ISO8601 문자열을 `summary` 와(지정 시) payload `forced_close_at` 에 포함. 미지정 시 payload 키 미포함.
- `tests/runtime/test_cli_operator.py`
- `test_enter_with_forced_close_at_maps_to_payload`(AC-5), `test_enter_without_forced_close_at_omits_field`(AC-6 하위호환), `test_enter_rejects_malformed_forced_close_at_before_submit`(AC-6 형식 거부) 추가.
- `docs/interface-contract.md`
- buy payload `forced_close_at` 항목에 CLI `--forced-close-at` 노출 설명 추가, 운영자 CLI 매핑 표 `enter` 행에 `--forced-close-at` 반영.
## Execution Log
1. `_build_parser` 의 `enter` 서브파서에 `--forced-close-at` 인자 추가(help: ISO8601 절대시각).
2. `_cmd_enter` 에 제출 전 파싱·검증 + payload 조건부 포함 로직 추가(기존 `datetime` import 재사용).
3. 단위 테스트 3종 추가.
4. interface-contract 정합 갱신.
5. `pytest tests/runtime/test_cli_operator.py` → 20 passed. 전체 회귀 `pytest`(KIS 샌드박스 env 테스트 deselect) → 353 passed / 35 skipped. `ruff check` → All checks passed.
## Validation
- 단위: `tests/runtime/test_cli_operator.py` 20 passed(신규 3종 포함).
- 회귀: 전체 단위 353 passed, 35 skipped(인프라 게이트 `TREX_POSTGRESQL_TEST_DSN`/`TREX_RABBITMQ_TEST_URL` 미설정), 1 deselected(KIS 샌드박스 env, 본 batch 무관).
- lint: `ruff check trex_core/runtime/cli.py tests/runtime/test_cli_operator.py` All passed.
## Remaining Risks
- naive(타임존 없는) timestamp 입력 시 핸들러 `_opt_dt`(`datetime.fromisoformat`) 규약을 그대로 따른다 — CLI 는 노출·형식 검증만 담당하고 타임존 정규화는 도입하지 않음(범위 밖). 운영자가 KST 의도를 명확히 하려면 오프셋 포함(`+09:00`) 권장(help 예시에 명시).
- 실 데몬 e2e(enter → admitted 슬롯 `forced_close_at` 설정 → REQ-037 만기 매도)는 verification 에서 다룬다.
@@ -0,0 +1,33 @@
# BAT-024: 운영자 `enter` 시간 기반 자동청산 옵션 — `--forced-close-at` (절대시간) 노출 (issue #17)
- Status: release-candidate
- Profile: batch-lite
- Source Discovery: dcy-016 (issue #17 change-req)
- Included REQ: REQ-032 (CHG-20260609-01)
- Excluded REQ: 없음
## Summary
- issue #17(`trex-core enter` 에 시간 기반 자동청산 옵션 부재) 를 batch-lite 로 delivery 한다. `enter` 는 `--stop-loss`/`--take-profit` 만 노출하고 `forced_close_at` 을 넣을 인자가 없어, 시간 기반 청산은 별도 수동/스케줄 `exit` 가 필요했다.
- 핵심: `trex_core/runtime/cli.py` `_cmd_enter` 에 **`--forced-close-at <ISO8601>` (절대시간)** 옵션 추가 + buy intent payload 에 파싱된 `forced_close_at` 매핑. buy intent 계약(REQ-007/021, 핸들러가 이미 `forced_close_at` optional 수용)·도메인(`Slot.forced_close_at`, REQ-066)·forced-close 만기 매도 루프(REQ-037)는 **이미 구현·불변** — 본 batch 는 CLI 노출 갭만 보완한다. 상대시간(`--hold-minutes`)은 미채택(사용자 결정).
## Scope
- In Scope:
- `_cmd_enter` 에 `--forced-close-at` 인자 추가(절대시간, ISO8601 파싱) + 잘못된 형식 제출 전 거부 (REQ-032 AC-5/6)
- buy intent payload 에 지정 시 `forced_close_at` 포함, 미지정 시 미포함(하위호환)
- `docs/interface-contract.md` enter 옵션 표 + payload `forced_close_at` 노출 정합
- `tests/runtime/test_cli_operator.py` enter `--forced-close-at` payload 매핑·형식 거부·미지정 회귀
- Out of Scope:
- 상대시간 옵션(`--hold-minutes`) — 미채택
- buy intent 계약/도메인/forced-close 루프 변경(이미 구현·불변)
- `exit`/`update_thresholds` 의 forced_close_at 노출(별도 주제)
## Documents
- Planning: [planning.md](./planning.md)
- Design: none (batch-lite with no architecture impact)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Profile **batch-lite**: 단일 파일(`cli.py`) 국소 변경 + 테스트/문서, 신규 계약/도메인/런타임 흐름 없음(기존 `forced_close_at` 경로 재사용), DB/스레드/마이그레이션 없음 → **Design Gate: no**(design.md 불요).
- REQ-032 는 CHG-20260609-01 로 **Approved**(신규 AC-5/6 미구현) 상태. 본 batch release-candidate + confirm-req-implemented 로 **Implemented** 복귀 예정.
- 발견 근거: GitHub issue #17.
@@ -0,0 +1,33 @@
# BAT-024 Planning
- Status: confirmed
- Batch: BAT-024
- Profile: batch-lite
## Included REQ
- REQ-032 (CHG-20260609-01) — 운영자 `enter` `--forced-close-at` (절대시간) 옵션
## Delivery Plan
1. `trex_core/runtime/cli.py` `_build_parser` 의 `enter` 서브파서에 `--forced-close-at`(dest `forced_close_at`, ISO8601 문자열) 인자 추가.
2. `_cmd_enter` 에서 `args.forced_close_at` 가 주어지면 `datetime.fromisoformat` 으로 파싱(잘못된 형식은 제출 전 `_fail` + 비정상 종료), buy intent payload 에 `forced_close_at`(ISO8601 문자열)으로 포함. 미지정 시 payload 에 키 미포함(하위호환).
3. `docs/interface-contract.md` 의 enter 옵션 목록/매핑에 `--forced-close-at` 추가.
4. `tests/runtime/test_cli_operator.py` 테스트 추가/갱신.
## Dependencies
- Internal:
- buy intent 핸들러 `_handle_buy_intent`(messaging_handlers.py:87) 가 이미 `forced_close_at` 수용 — 변경 불필요.
- 도메인 `Slot.forced_close_at`(REQ-066), forced-close 만기 매도 루프(REQ-037) — 변경 불필요.
- External:
- 없음.
## Design Gate
- Design Required Before Implementation: no
- Reason: 단일 파일(`cli.py`) 국소 변경. 신규 계약 필드/도메인/런타임 흐름 없음(기존 `forced_close_at` 경로 재사용). 구조·인터페이스 구조 변경 없음(옵션 additive). 설계 판단이 change-req(CHG-20260609-01) AC-5/6 에 완비.
## Milestones
1. CLI 인자 + payload 매핑 + 형식 검증 구현.
2. 단위 테스트(payload 매핑/형식 거부/미지정 회귀) green + interface-contract 정합.
## Risks
- R-1: timestamp 파싱 모호성(타임존 없는 naive vs aware). 대응: `datetime.fromisoformat` 로 파싱하되 payload 에는 입력 문자열을 ISO8601 로 전달(핸들러 `_opt_dt` 와 동일 규약). naive/aware 처리는 기존 핸들러 규약을 따른다 — 본 batch 는 CLI 노출만 담당.
- R-2: 잘못된 형식이 데몬까지 가서 admission 실패로 나타나는 회귀. 대응: CLI 제출 **전** 파싱 검증 후 거부(AC-6).
@@ -0,0 +1,39 @@
# BAT-024 Verification
- Status: approved
- Batch: BAT-024
- Profile: batch-lite
## Acceptance Mapping
### REQ-032 (CHG-20260609-01) — 운영자 `enter --forced-close-at`
| AC | 검증 | Evidence |
|----|------|----------|
| AC-5: `--forced-close-at <ISO8601>` → buy intent payload `forced_close_at` 매핑 | `enter PF-1 --forced-close-at 2026-06-09T15:20:00+09:00 --yes` → payload `forced_close_at` 동일값 | `tests/runtime/test_cli_operator.py::test_enter_with_forced_close_at_maps_to_payload` → PASS |
| AC-6a: 미지정 시 payload 미포함(하위호환) | `enter PF-1 --yes` → payload 에 `forced_close_at` 키 없음 | `::test_enter_without_forced_close_at_omits_field` → PASS |
| AC-6b: 잘못된 형식 제출 전 거부 | `enter PF-1 --forced-close-at notatime --yes` → rc=1, RPC 미호출 | `::test_enter_rejects_malformed_forced_close_at_before_submit` → PASS |
### 회귀 (기존 AC-1~4 + enter/exit/universe/slots 불변)
- `tests/runtime/test_cli_operator.py` 20 passed(기존 17 + 신규 3) — pause/resume/universe/slots/enter/exit/안전장치 전부 green.
- 전체 단위 회귀 353 passed / 35 skipped / 1 deselected.
## 다운스트림 경로 (이미 구현·검증, 본 batch 무변경)
- buy intent 핸들러가 payload `forced_close_at` 수용: `trex_core/application/messaging_handlers.py:87`(`_opt_dt`) — REQ-007/021, 기존 구현.
- 도메인 `Slot.forced_close_at`(REQ-066) + forced-close 만기 매도 루프(REQ-037, BAT-010/011 검증 완료). 본 batch 는 CLI 노출만 추가하므로 다운스트림 재검증 불요.
## 검증 명령/결과
- `pytest tests/runtime/test_cli_operator.py -q` → **20 passed**.
- `pytest -q --deselect tests/adapters/broker/test_kis.py::test_sandbox_token_and_submit` → **353 passed, 35 skipped, 1 deselected**.
- `ruff check trex_core/runtime/cli.py tests/runtime/test_cli_operator.py` → All checks passed.
## Baseline 문서 동기화
- `docs/interface-contract.md` — buy payload `forced_close_at` 의 CLI `--forced-close-at` 노출 + 운영자 CLI 매핑 표 `enter` 행 갱신. (runtime-flows 는 흐름 무변경 — forced-close 경로 기존 그대로, 추가 갱신 불요.)
## 검증 결과 요약
- AC-5/6 전부 단위 PASS, 기존 AC·명령 회귀 green, baseline 정합 완료.
- **Ready for Release: true.**
## 미검증/한계
- 실 데몬 + LIVE/paper full-stack e2e(enter `--forced-close-at` → admitted 슬롯 `forced_close_at` 설정 → REQ-037 만기 시 시장가 청산)는 본 batch 의 책임 경계(=올바른 payload 필드 방출) 밖이며, 다운스트림(REQ-037)은 BAT-010/011 에서 이미 검증됨. CLI 책임은 payload 매핑 단위로 직접 커버.
- pytest 인프라 게이트(`TREX_POSTGRESQL_TEST_DSN`/`TREX_RABBITMQ_TEST_URL`)는 본 변경과 무관(CLI 는 RPC 주입 단위 테스트, DB/브로커 불요) — skipped 35 는 본 batch 범위 밖.
+1
View File
@@ -31,3 +31,4 @@
| BAT-021 | standard | release-candidate | REQ-034 (CHG-20260608-02), REQ-021 (CHG-20260608-03), REQ-037 (CHG-20260608-01), REQ-038 (CHG-20260608-01), REQ-039 (CHG-20260608-01), REQ-032 (CHG-20260608-02), REQ-042 (CHG-20260608-01) | dcy-017 | [bat-021](./bat-021_20260608_watchdog-ownership-visibility/) |
| BAT-022 | standard | release-candidate | REQ-028 (CHG-20260608-01), REQ-032 (CHG-20260608-03) | dcy-016 | [bat-022](./bat-022_20260608_slot-state-snapshot-visibility/) |
| BAT-023 | standard | release-candidate | REQ-017 (CHG-20260608-02), REQ-019 (CHG-20260608-02), REQ-029 (CHG-20260608-01), REQ-030 (CHG-20260608-01, Approved→Implemented) | dcy-018 | [bat-023](./bat-023_20260608_ledger-cash-position-writeback-option-d/) |
| BAT-024 | batch-lite | release-candidate | REQ-032 (CHG-20260609-01) | dcy-016 (issue #17) | [bat-024](./bat-024_20260609_operator-enter-forced-close-at/) |