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:
@@ -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 범위 밖.
|
||||
@@ -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/) |
|
||||
|
||||
@@ -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 <ISO8601>`(절대시간)로 설정하며(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 <portfolio_id> [--capital/--qty/...]` | `trex.intent.req[.{instance}]` | buy intent(`portfolio_id` 필수, symbol 서버 resolve) | 안전장치 필수 |
|
||||
| `enter <portfolio_id> [--capital/--qty/--stop-loss/--take-profit/--forced-close-at ...]` | `trex.intent.req[.{instance}]` | buy intent(`portfolio_id` 필수, symbol 서버 resolve; `--forced-close-at <ISO8601>`→`forced_close_at`, REQ-032 CHG-20260609-01) | 안전장치 필수; `--forced-close-at` 형식은 제출 전 검증 |
|
||||
| `exit <slot_id> [--price]` | `trex.intent.req[.{instance}]` | sell intent(`slot_id`, optional `limit_price`) | 가격 미지정 시 시장가 |
|
||||
| `pause/resume/paused` | (RPC 아님) | — | direct-DB 유지(OQ-3) |
|
||||
|
||||
|
||||
@@ -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 <ISO8601>` (절대시간)** 옵션을 제공한다. 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 <portfolio_id> ... --forced-close-at <ISO8601>`, 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 <ISO8601>` (절대시간) 옵션** 추가. 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(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 <portfolio_id> ... --forced-close-at <ISO8601 timestamp>` 가 주어지면 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 <ts>` → 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.
|
||||
|
||||
+2
-1
@@ -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 <ISO8601>` (절대시간) 옵션 추가 — 운영자가 매수 진입 명령 하나로 시간 기반 자동청산(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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user