From 1cbbb89091f523d568ab2dcca787c86c644be351 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Tue, 9 Jun 2026 13:10:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(cli):=20enter=20--forced-close-at=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EA=B8=B0=EB=B0=98=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B2=AD=EC=82=B0=20=EC=98=B5=EC=85=98=20(issue=20#17,=20BAT-0?= =?UTF-8?q?24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trex-core enter 에 --forced-close-at (절대시간) 옵션 추가. 지정 시 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 --- .../implementation.md | 32 +++++++ .../index.md | 33 +++++++ .../planning.md | 33 +++++++ .../verification.md | 39 ++++++++ docs/batches/index.md | 1 + docs/interface-contract.md | 3 +- .../req-032_runtime-pause-resume-control.md | 89 ++++++++++++++++++- docs/srs/index.md | 3 +- tests/runtime/test_cli_operator.py | 26 ++++++ trex_core/runtime/cli.py | 21 ++++- 10 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 docs/batches/bat-024_20260609_operator-enter-forced-close-at/implementation.md create mode 100644 docs/batches/bat-024_20260609_operator-enter-forced-close-at/index.md create mode 100644 docs/batches/bat-024_20260609_operator-enter-forced-close-at/planning.md create mode 100644 docs/batches/bat-024_20260609_operator-enter-forced-close-at/verification.md diff --git a/docs/batches/bat-024_20260609_operator-enter-forced-close-at/implementation.md b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/implementation.md new file mode 100644 index 0000000..171fea5 --- /dev/null +++ b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/implementation.md @@ -0,0 +1,32 @@ +# BAT-024 Implementation + +- Status: completed +- Batch: BAT-024 + +## Plan Summary +- 운영자 CLI `enter` 에 `--forced-close-at `(절대시간) 옵션을 추가하고, 지정 시 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 에서 다룬다. diff --git a/docs/batches/bat-024_20260609_operator-enter-forced-close-at/index.md b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/index.md new file mode 100644 index 0000000..1d0d574 --- /dev/null +++ b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/index.md @@ -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 ` (절대시간)** 옵션 추가 + 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. diff --git a/docs/batches/bat-024_20260609_operator-enter-forced-close-at/planning.md b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/planning.md new file mode 100644 index 0000000..90fe5d0 --- /dev/null +++ b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/planning.md @@ -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). diff --git a/docs/batches/bat-024_20260609_operator-enter-forced-close-at/verification.md b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/verification.md new file mode 100644 index 0000000..72a5626 --- /dev/null +++ b/docs/batches/bat-024_20260609_operator-enter-forced-close-at/verification.md @@ -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 ` → 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 범위 밖. diff --git a/docs/batches/index.md b/docs/batches/index.md index f58e13e..616316d 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -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/) | diff --git a/docs/interface-contract.md b/docs/interface-contract.md index 8f42c9c..5d7db3a 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -89,6 +89,7 @@ ``` - **`portfolio_id` 필수(dcy-011/REQ-007/021)**: 매수의 정식 식별자. 누락 시 admission 에서 `portfolio_id_required` 로 거부. **`symbol` 은 client payload 에 없다** — core 가 admission 시 universe(`SnapshotStore`)에서 `portfolio_id → symbol` 을 resolve 해 슬롯/KIS 주문에 사용한다. universe 에 portfolio_id 가 없으면 `unknown_portfolio` 로 거부(하위호환·symbol fallback 없음, hard cutover). - `stop_loss_price`/`take_profit_price`(선택, REQ-035/038): 매수 시 설정하면 워치독 만료 후 safety-sell 가격 트리거의 기준이 된다. 미포함 시 NULL(트리거 비활성). +- `forced_close_at`(선택, REQ-066/037): 시간 기반 자동청산(만기) 절대시각. 운영자 CLI `enter --forced-close-at `(절대시간)로 설정하며(REQ-032 CHG-20260609-01, issue #17, BAT-024), 미지정 시 payload 에 미포함(NULL). 설정 시 forced-close 만기 매도 루프(REQ-037)가 그 시각에 시장가 청산한다. 상대시간 옵션은 제공하지 않는다. sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason":..., "sell_strategy_id":..., "limit_price": float?}`. - **`limit_price` 선택(dcy-012/REQ-007)**: 매도 지정가. **지정 시 지정가 매도(`ORD_DVSN="00"`, 그 가격)**, **미지정 시 시장가 매도(`ORD_DVSN="01"`)**. 자율 매도(safety-sell/kill-switch)는 가격을 싣지 않아 항상 시장가로 체결된다. 매수가(과거 `entry_price`)는 매도 가격으로 **재사용하지 않는다**(issue #10 — 슬롯 `limit_price` 는 pending 이탈 시 clear). buy payload 의 `entry_price` 필드명은 본 주기에서 유지(하위호환). @@ -202,7 +203,7 @@ sell payload 는 `{"intent_type":"sell", "intent_id":..., "slot_id":..., "reason |---|---|---|---| | `universe` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | 자산 목록 조회(워치독 억제) | | `slots [--status all\|holding\|buyable\|pending]` | `trex.snapshot.req[.{instance}]` | `{view:"all", actor:"operator", touch_watchdog:false}` | envelope `slots[]`(실제 슬롯 상태) 표시(REQ-028 AC-4/REQ-032, BAT-022) — universe `assets[]` 아님. `--status` 는 슬롯 status 기준 client-side 필터 | -| `enter [--capital/--qty/...]` | `trex.intent.req[.{instance}]` | buy intent(`portfolio_id` 필수, symbol 서버 resolve) | 안전장치 필수 | +| `enter [--capital/--qty/--stop-loss/--take-profit/--forced-close-at ...]` | `trex.intent.req[.{instance}]` | buy intent(`portfolio_id` 필수, symbol 서버 resolve; `--forced-close-at `→`forced_close_at`, REQ-032 CHG-20260609-01) | 안전장치 필수; `--forced-close-at` 형식은 제출 전 검증 | | `exit [--price]` | `trex.intent.req[.{instance}]` | sell intent(`slot_id`, optional `limit_price`) | 가격 미지정 시 시장가 | | `pause/resume/paused` | (RPC 아님) | — | direct-DB 유지(OQ-3) | diff --git a/docs/srs/Interface/req-032_runtime-pause-resume-control.md b/docs/srs/Interface/req-032_runtime-pause-resume-control.md index b74e9da..02f4dae 100644 --- a/docs/srs/Interface/req-032_runtime-pause-resume-control.md +++ b/docs/srs/Interface/req-032_runtime-pause-resume-control.md @@ -14,17 +14,20 @@ - pause 상태는 **Postgres 에 영속**(portfolio_id 키)하며 daemon 재시작 후에도, 일일 스냅샷 refresh 후에도 유지된다(refresh 는 universe/포지션만 갱신, pause overlay 불변). - 적용 범위는 **전역** — pause 된 자산은 모든 view/전략 요청에서 마스킹된다. - pause 는 request-time mask 이므로 resume 시 다음 요청부터 즉시 응답에 복귀한다. +- (CHG-20260609-01, issue #17) 운영자 CLI `enter` 는 시간 기반 자동청산을 위해 **`--forced-close-at ` (절대시간)** 옵션을 제공한다. CLI 는 이를 buy intent payload 의 기존 `forced_close_at` 필드에 매핑하며, 도메인·forced-close 루프(REQ-037)는 불변이다. 상대시간 옵션은 채택하지 않는다. ## Acceptance Criteria 1. Given 운영자가 `portfolio_id` 를 pause, When 임의 `view`/`strategy_key` 로 스냅샷을 요청, Then 그 자산은 응답에서 마스킹된다. 2. Given pause 상태에서 daemon 재시작 또는 일일 refresh 발생, When 이후 요청, Then pause 가 유지된다. 3. Given 운영자가 resume, When 다음 요청, Then 그 자산이 응답에 다시 포함된다. 4. Given CLI 로 pause 후 RPC 제어로 조회(또는 그 반대), Then 동일한 pause 상태가 보인다. +5. (CHG-20260609-01) Given 운영자가 `trex-core enter ... --forced-close-at `, When buy intent 가 제출·admit, Then payload 에 파싱된 `forced_close_at` 절대시각이 실리고 admitted 슬롯의 `forced_close_at` 이 그 값으로 설정된다. +6. (CHG-20260609-01) Given `--forced-close-at` 미지정, Then payload 에 `forced_close_at` 을 포함하지 않고 기존 enter 동작이 불변이다. Given 잘못된 timestamp 형식, Then 제출 전 검증 오류로 거부된다. ## Impacted Area - Docs: docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md -- Modules: pause 상태 store(Postgres), CLI 제어, dcy-006 RPC 제어 메시지 핸들러, 응답 산출 시 pause 마스킹 -- Tests: pause→호출→resume→호출 시퀀스, 재시작/refresh 후 영속 +- Modules: pause 상태 store(Postgres), CLI 제어, dcy-006 RPC 제어 메시지 핸들러, 응답 산출 시 pause 마스킹, (CHG-20260609-01) `trex_core/runtime/cli.py` `_cmd_enter` `--forced-close-at` 옵션 → buy intent `forced_close_at` 매핑 +- Tests: pause→호출→resume→호출 시퀀스, 재시작/refresh 후 영속, (CHG-20260609-01) enter `--forced-close-at` payload 매핑·형식 검증 ## Notes - pause(수동·영속) vs warning(자동·일시, REQ-031)은 독립 overlay 이며 OR 합성(둘 중 하나라도면 마스킹). @@ -285,3 +288,85 @@ #### Validation Plan - tests/runtime/test_cli_operator.py::test_slots_shows_real_slot_state_not_universe / ::test_slots_status_filters_by_slot_status / ::test_slots_marks_watchdog_owned. 운영 재현(BAT-022): 신규 코드 데몬 + 실 인프라 `trex-core slots` → slots=10 status·`--status holding`=0·universe(assets=1) 분리·비침습. pytest 인프라 게이트는 Human Approval Memo 수용 잔여. + +### CHG-20260609-01 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- (change-req, issue #17) `trex-core enter` 에 **`--forced-close-at ` (절대시간) 옵션** 추가. 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각을 지정. CLI 는 이 값을 buy intent payload 의 **기존** `forced_close_at` 필드에 매핑한다(계약: REQ-007/021 buy intent body 가 이미 `forced_close_at` optional 수용, 도메인 `Slot.forced_close_at`(REQ-066), forced-close 만기 매도 루프 REQ-037 — 전부 구현·불변). 상대시간(`--hold-minutes`)은 **미채택**(사용자 결정: 절대시간만). + +#### Intent + +- 운영자가 진입 명령 하나로 시간 기반 매매 계획(자동청산 시각 포함)을 표현하게 한다. 현재는 `--stop-loss`/`--take-profit` 만 노출돼 시간 기반 청산은 별도 수동/스케줄 `exit` 가 필요했다(issue #17 갭). 도메인 능력은 이미 존재하고 CLI 노출만 보완. + +#### Acceptance Criteria Delta + +- Added: + - AC-5: `trex-core enter ... --forced-close-at ` 가 주어지면 CLI 는 buy intent payload 에 `forced_close_at`(파싱된 절대시각)을 실어 제출하고, admitted 슬롯의 `forced_close_at` 이 그 값으로 설정된다. + - AC-6: `--forced-close-at` 미지정 시 payload 에 `forced_close_at` 을 포함하지 않으며 기존 enter 동작은 불변(하위호환). 잘못된 timestamp 형식은 제출 **전** 검증 오류로 거부한다. +- Updated: none +- Removed: none + +#### Impacted Area Delta + +- Modules: `trex_core/runtime/cli.py`(`_cmd_enter` payload 에 `forced_close_at` 추가 + `--forced-close-at` 인자/파싱·검증). buy intent 핸들러·도메인·forced-close 루프는 불변(이미 수용). +- Docs: `docs/interface-contract.md`(enter 옵션 표 + buy intent payload `forced_close_at` 노출), 필요 시 `docs/runtime-flows.md`. +- Tests: `tests/runtime/test_cli_operator.py`(enter `--forced-close-at` → payload `forced_close_at` 매핑 + 잘못된 형식 거부). + +#### Delivery Trace Delta + +- Batches: 신규 batch-lite(차기 BAT) — enter `--forced-close-at` 노출. +- Releases: none (차기 release 또는 통합 편입). + +#### Revalidation Impact + +- Reverification Needed: yes (신규 batch 에서 AC-5/6 검증) +- Existing Implementation Invalidated: no (pause/resume/universe/slots/enter/exit 기존 AC 불변, 옵션 additive) +- Status Recommendation: Implemented → Approved (신규 AC-5/6 미구현 — batch 재검증 후 Implemented 복귀; 기존 AC-1~4 및 CLI 명령은 구현 유지) +- Follow-up Action: draft-batch(batch-lite) → run-batch-implementation → verification → confirm-req-implemented + +#### Validation Plan + +- 신규 BAT: `enter --forced-close-at ` → buy intent payload 에 `forced_close_at` 정확 매핑 단위, 잘못된 형식(예 `--forced-close-at notatime`) 제출 전 거부 단위, 미지정 시 payload 미포함(회귀). (가능 시) 실 데몬 e2e: `enter --forced-close-at <근접시각>` → admitted 슬롯 `forced_close_at` 설정 → forced-close 루프(REQ-037) 만기 매도 확인. + +### CHG-20260609-02 + +- Date: 2026-06-09 +- Author: Justin Ko + +#### Change Summary + +- Status 변경: Approved → Implemented. BAT-024 release-candidate verification evidence 기반(CHG-20260609-01 AC-5/6). + +#### Intent + +- 운영자 `enter --forced-close-at`(절대시간) → buy intent `forced_close_at` 매핑 구현 완료 동기화. + +#### Acceptance Criteria Delta + +- Added/Updated/Removed: 없음 + +#### Impacted Area Delta + +- Docs/Modules/Tests: 없음 + +#### Delivery Trace Delta + +- Batches: + - BAT-024 (release-candidate) +- Releases: + - none + +#### Revalidation Impact + +- Reverification Needed: no (BAT-024 에서 완료) +- Existing Implementation Invalidated: no +- Status Recommendation: Implemented +- Follow-up Action: release 단계 별도 진행(차기 REL 또는 통합 편입) + +#### Validation Plan + +- 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 형식 거부). 전체 단위 353 passed, ruff clean. diff --git a/docs/srs/index.md b/docs/srs/index.md index 27f50c4..16c38e4 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -112,7 +112,7 @@ | REQ-029 | 자산 선별(slot overlay) + view 모드 + strategy_key 필터 | Interface | Implemented | High | project maintainers | [req-029](./Interface/req-029_asset-selection-slot-overlay-view-and-strategy-key.md) | | REQ-030 | 일일 스냅샷 스케줄러 + 옵션 D 현금/포지션 갱신 | Integration | Implemented | High | project maintainers | [req-030](./Integration/req-030_daily-snapshot-scheduler-and-option-d-cash-position.md) | | REQ-031 | 포지션 정합 검증 + warning 마스킹 | Exception | Implemented | High | project maintainers | [req-031](./Exception/req-031_position-reconciliation-and-warning.md) | -| REQ-032 | 런타임 pause/resume 제어 | Interface | Implemented | Medium | project maintainers | [req-032](./Interface/req-032_runtime-pause-resume-control.md) | +| REQ-032 | 런타임 pause/resume 제어 (+운영자 CLI 표면) | Interface | Implemented | Medium | project maintainers | [req-032](./Interface/req-032_runtime-pause-resume-control.md) | | REQ-033 | 스냅샷 read 레이어 baseline 문서 정합 | Documentation | Implemented | Medium | project maintainers | [req-033](./Documentation/req-033_snapshot-read-baseline-alignment.md) | | REQ-034 | per-slot 워치독 — snapshot RPC 기반 전략 모듈 활성 감지 | Interface | Implemented | High | project maintainers | [req-034](./Interface/req-034_per-slot-watchdog.md) | | REQ-035 | 슬롯 가격 임계값 도메인 필드 + DB 마이그레이션 | Data | Implemented | High | project maintainers | [req-035](./Data/req-035_slot-price-threshold-fields-and-migration.md) | @@ -139,6 +139,7 @@ ## Recent Change Log Summary +- 2026-06-09 :: REQ-032 change-req→Implemented (CHG-20260609-01/02, issue #17, BAT-024): `trex-core enter` 에 `--forced-close-at ` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약 REQ-007/021·도메인 REQ-066·forced-close 루프 REQ-037 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 제출 전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택. AC-5/6 신규. batch-lite(BAT-024) 단위 검증 → **Implemented**. 단위 353 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (dcy-018, BAT-023): ledger write 응답의 권위 cash/quantity 를 portfolio 스냅샷에 write-back(option D 활성화) — 매수/매도 직후 "수량 live·현금 stale" 비대칭 제거. `record_trade`→`LedgerWriteOutcome{result, *_after}`+어댑터 `cash_balance_after`/`asset_quantity_after` 파싱(new+duplicate)(REQ-017), `LedgerWriter` SnapshotStore write-back best-effort+`pending_cash_delta`(REQ-019), 추정 현금 오버레이(`cash_estimated` 라벨)+슬롯 오버레이 유지(REQ-029, OQ-1 B/OQ-4 i), option D 활성화로 REQ-030 Approved→Implemented. 전부 change-req(신규 REQ 없음, OQ-3 a). portfolio-hub#8(`*_after`) 동작 중. 단위 350 passed, 인프라 게이트 385 passed, ruff clean. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-028 + REQ-032 change-req→Implemented (CHG-20260608-01/02 / -03/04, dcy-016 후속, BAT-022): `trex-core slots` 가 universe `assets`(portfolio)를 슬롯 상태로 오인 표시하던 갭 정정(10개 슬롯인데 universe 1개·status 없음 — BAT-007 HTTP 슬롯 read 제거 후 슬롯 상태 read 표면 부재가 근인). snapshot envelope 에 read-only per-slot `slots[]`(canonical `slot_to_dict`: slot_id/trading_date/status/symbol/qty/avg_price/entry_price/.../portfolio_id) additive 추가(REQ-028 AC-4, strategy `assets[]` 불변), `trex-core slots` 가 `slots[]`(실제 슬롯 상태) 표시·`--status` 슬롯 status 필터·`universe` 는 `assets[]` 유지(REQ-032). BAT-007 stub/BAT-008 envelope 슬롯 스키마 단일화. 신규 코드 데몬 + 실 인프라 운영 재현 PASS(slots=10·status·DB 일치·universe 분리·비침습). 단위 338 passed, ruff clean. pytest 인프라 게이트는 Human Approval Memo 수용. 둘 다 **Implemented**. (change-req/confirm-req-implemented) - 2026-06-08 :: REQ-034/021/037/038/039/032/042 change-req (dcy-017, BAT-021): 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태를 가시화(issue #12). snapshot RPC 응답에 holding 슬롯 워치독 상태(expired/last_seen) read-only 동봉(best-effort) + `WatchdogStore.last_seen`(REQ-034/021), `trex-core slots` WATCHDOG-OWNED 표시(REQ-032), safety-sell 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO) 로그+in-process 카운터(REQ-037/038/039), baseline 2문서 정합(REQ-042). best-effort(청산 무차단), Redis 미채택. 전부 change-req(신규 REQ 없음, OQ-3). 단위 335 passed, 인프라 게이트 369 passed, ruff clean. (change-req/confirm-req-implemented) diff --git a/tests/runtime/test_cli_operator.py b/tests/runtime/test_cli_operator.py index fcb9071..7b4f2c7 100644 --- a/tests/runtime/test_cli_operator.py +++ b/tests/runtime/test_cli_operator.py @@ -143,6 +143,32 @@ def test_enter_with_yes_submits_buy_intent(): assert "symbol" not in p # server-resolved (dcy-011) +def test_enter_with_forced_close_at_maps_to_payload(): + # REQ-032 AC-5: --forced-close-at (absolute) → buy intent forced_close_at. + rpc = _RpcRecorder(_ADMITTED) + rc = cli.main(["enter", "PF-1", "--capital", "100000", "--yes", + "--forced-close-at", "2026-06-09T15:20:00+09:00"], rpc=rpc) + assert rc == 0 + p = rpc.calls[0]["payload"] + assert p["forced_close_at"] == "2026-06-09T15:20:00+09:00" + + +def test_enter_without_forced_close_at_omits_field(): + # REQ-032 AC-6: unspecified → field absent (backward compat). + rpc = _RpcRecorder(_ADMITTED) + rc = cli.main(["enter", "PF-1", "--capital", "100000", "--yes"], rpc=rpc) + assert rc == 0 + assert "forced_close_at" not in rpc.calls[0]["payload"] + + +def test_enter_rejects_malformed_forced_close_at_before_submit(): + # REQ-032 AC-6: invalid timestamp rejected at CLI, no RPC submitted. + rpc = _RpcRecorder(_ADMITTED) + rc = cli.main(["enter", "PF-1", "--capital", "100000", "--yes", + "--forced-close-at", "notatime"], rpc=rpc) + assert rc == 1 and rpc.calls == [] + + def test_enter_rejected_returns_nonzero(): rpc = _RpcRecorder(_REJECTED) rc = cli.main(["enter", "PF-x", "--yes"], rpc=rpc) diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 3805253..7ea75a1 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -179,8 +179,19 @@ def _intent_guard(args, cfg, summary: dict, action: str, confirm) -> int | None: def _cmd_enter(args, cfg, rpc, confirm) -> int: + # REQ-032 AC-6: validate the absolute auto-close timestamp BEFORE submitting, + # so a malformed value is rejected at the CLI instead of failing admission. + forced_close_at = getattr(args, "forced_close_at", None) + if forced_close_at is not None: + try: + forced_close_at = datetime.fromisoformat(forced_close_at).isoformat() + except ValueError: + _fail(f"invalid --forced-close-at (expected ISO8601): {args.forced_close_at!r}", + args.json) + return 1 summary = {"portfolio_id": args.portfolio_id, "qty": args.qty, - "allocated_capital": args.capital, "entry_price": args.entry_price} + "allocated_capital": args.capital, "entry_price": args.entry_price, + "forced_close_at": forced_close_at} short = _intent_guard(args, cfg, summary, "enter(buy)", confirm) if short is not None: return short @@ -192,6 +203,9 @@ def _cmd_enter(args, cfg, rpc, confirm) -> int: "sell_strategy_id": args.sell_strategy_id, "stop_loss_price": args.stop_loss, "take_profit_price": args.take_profit, } + # REQ-032 AC-5/6: only carry forced_close_at when specified (backward compat). + if forced_close_at is not None: + payload["forced_close_at"] = forced_close_at resp = rpc(topology.intent_queue(_resolve_instance(args)), payload, correlation_id=payload["intent_id"], timeout=args.timeout) return _report_intent(resp, args.json) @@ -274,6 +288,11 @@ def _build_parser() -> argparse.ArgumentParser: p_enter.add_argument("--strategy-id", dest="strategy_id", default=None) p_enter.add_argument("--stop-loss", dest="stop_loss", type=float, default=None) p_enter.add_argument("--take-profit", dest="take_profit", type=float, default=None) + p_enter.add_argument( + "--forced-close-at", dest="forced_close_at", default=None, + help="absolute auto-close timestamp (ISO8601, e.g. 2026-06-09T15:20:00+09:00) " + "→ buy intent forced_close_at", + ) _add_intent_guards(p_enter) _add_common(p_enter)