feat(slot): slot entry_count + closed-slot reallocation (dcy-019, BAT-025) + REL-005

dcy-019/issue #18: slot 자원을 진입 횟수 기준으로 고르게 재사용하고 idle 소진 시
closed 슬롯을 직접 재할당한다. 거버넌스는 전부 change-req(신규 REQ 없음).

- 도메인(REQ-002): Slot.entry_count(기본 0) + EXPECTED_SLOT_FIELDS, 상태 머신
  closed→pending_buy 신규 전이(base 8→9, ALLOWED_TRANSITIONS 14→15),
  _apply_transition_fields closed→pending_buy clean-slate + →pending_buy 시 +1
- 영속화(REQ-006): 멱등 마이그레이션 008(entry_count), flat 매핑,
  find_allocatable_slot(idle∪closed, entry_count→updated_at→slot_id) + in-memory 동등
- admission(REQ-007): _admit_buy find_idle_slot→find_allocatable_slot + from_state=slot.status
- 테스트(REQ-003): 전이 카운트/계약 가드 정합 + test_slot_entry_count.py / test_buy_admission_reallocation.py
- 문서(REQ-009): data-model.md / runtime-flows.md 정합
- 기존 daily_symbol_entries(symbol-scoped) 분리·미변경

BAT-025(release-candidate) delivery + change-req REQ-002/003/006/007/009(CHG-20260609-01).
REL-005(confirmed, app-service): BAT-024+BAT-025 묶음. operator-facing run-the-app
스모크로 enter --forced-close-at(BAT-024)·closed 재할당(BAT-025) 실 데몬 검증
(실 PG 마이그레이션 008 round-trip 포함).

검증: 단위 366 passed / 35 skipped(KIS sandbox 1건 deselect — live .env 환경 의존), ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 14:49:22 +09:00
co-authored by Claude Opus 4.8
parent 941d324899
commit 6e8e79660c
28 changed files with 912 additions and 26 deletions
+1
View File
@@ -12,3 +12,4 @@
| REL-002 | tooling | feedback-captured | BAT-009 | Change-req: paper execution_id Redis(#4) | [rel-002](rel-002_20260606_paper-broker-env-tuning.md) |
| REL-003 | app-service | feedback-captured | BAT-010, BAT-011, BAT-012, BAT-013 | dcy-007 (자율 안전 매도) + dcy-008 (kill-switch) + dcy-009 (다중 인스턴스 토폴로지) — 통합 릴리즈 | [rel-003](rel-003_20260607_safety-killswitch-multi-instance.md) |
| REL-004 | app-service | feedback-captured | BAT-014, BAT-015, BAT-016, BAT-017, BAT-018, BAT-019, BAT-020, BAT-021, BAT-022, BAT-023 | dcy-010 (safety-sell 쿨다운) + dcy-011~017 (buy/sell 계약·KIS 취소·trading_date TZ·ledger 백오프 버그픽스 + 운영자 CLI + 워치독 가시화) + dcy-016 후속(슬롯 상태 가시화) + dcy-018 (ledger 현금/포지션 write-back, option D) — 통합 릴리즈 | [rel-004](rel-004_20260608_safety-cooldown-bugfix-hardening-cli.md) |
| REL-005 | app-service | confirmed | BAT-024, BAT-025 | dcy-016 후속/issue #17 (operator enter --forced-close-at) + dcy-019/issue #18 (slot entry_count + closed 슬롯 재할당) — 통합 릴리즈 | [rel-005](rel-005_20260609_operator-forced-close-and-slot-reallocation.md) |
@@ -0,0 +1,117 @@
# REL-005: operator enter --forced-close-at + slot entry_count·closed 재할당 통합 릴리즈
- Status: confirmed
- Profile: app-service
- Included Batch: BAT-024, BAT-025
## Scope
- Included:
- BAT-024 (dcy-016 후속 / issue #17 / REQ-032 CHG-20260609-01): 운영자 `trex-core enter` 에 `--forced-close-at <ISO8601>`(절대시간) 옵션 추가 — 매수 진입 명령 하나로 시간 기반 자동청산(forced_close) 시각 지정. CLI 가 buy intent payload 의 기존 `forced_close_at` 필드에 매핑(계약·도메인·forced-close 루프 이미 구현·불변, CLI 노출 갭만 보완). 잘못된 형식 사전 거부, 미지정 시 미포함(하위호환). 상대시간(`--hold-minutes`) 미채택.
- BAT-025 (dcy-019 / issue #18 / REQ-002·003·006·007·009 CHG-20260609-01): slot-scoped `entry_count`(기본 0) 필드 + 멱등 마이그레이션 008, 상태 머신 `closed→pending_buy` 신규 전이(base 8→9), buy admission 후보를 idle∪closed 로 확장하고 `entry_count→updated_at→slot_id` 결정적 정렬로 선택(`find_allocatable_slot`) + closed 직접 재할당, `→pending_buy` 시 entry_count +1 / closed→pending_buy clean-slate 점유 초기화. 기존 `daily_symbol_entries`(symbol-scoped) 분리·미변경.
- Excluded:
- BAT-001~023 (REL-001/002/003/004 에서 이미 released)
- `daily_symbol_entries` 제거(dcy-019 Out of Scope — 별도 REQ-002 change-req 트랙)
- 운영자 CLI/스냅샷에 slot `entry_count` 노출 (dcy-019 §5 제외 — 후속 후보)
- snapshot `available_slots`(=idle_count) capacity 의미 변경 (dcy-019 범위 밖)
## Rollout Plan
1. 배포 전: 대상 인스턴스 데몬 정지(또는 거래 세션 외 시간), 현재 리비전 SHA 기록.
2. DB 마이그레이션 적용(멱등): `008_add_slot_entry_count.sql`(daily_slot 에 `entry_count INTEGER NOT NULL DEFAULT 0` additive 추가). 데몬 기동 시 `apply_migrations` 가 자동 적용. additive 컬럼이라 기존 행은 0 으로 채워짐(무해).
3. 신 리비전(REL-005 배포 SHA) 배포 + 데몬 재기동(`--config <inst>.toml`). 기동 로그에서 RUNNING/비-degraded 확인.
4. 스냅샷 RPC degraded=false·available_slots·asset_count 확인. trading_date 가 KST 당일과 일치하는지 확인.
5. 운영자 CLI 스모크(paper 또는 비거래 시간):
- `trex-core enter <pf> --capital ... --forced-close-at 2026-06-09T15:00:00+09:00 --yes` → admitted + 슬롯 `forced_close_at` 설정 확인(BAT-024). 잘못된 형식(`--forced-close-at zzz`)은 제출 전 거부.
- idle 슬롯 소진 후(또는 일부 closed 상태에서) 추가 buy → closed 슬롯 재할당으로 admit 되고 `no_idle_slot` 거부가 사라지는지 확인(BAT-025). 슬롯 회전이 `entry_count` 기준 고르게 분산되는지 관측.
6. 거래 세션 재개.
## Rollback Plan
- 조건: 기동 실패, 마이그레이션 오류, buy admission/슬롯 선택 회귀(잘못된 슬롯 재사용·상태 오염), CLI `--forced-close-at` 오작동.
- 절차: 신 리비전 → 직전 released 리비전(REL-004 시점, `fec9411`)으로 롤백 + 데몬 재기동.
- 마이그레이션: 008 은 **additive(ADD COLUMN)** 라 구 코드와 공존 가능(구 코드는 `entry_count` 컬럼을 읽지/쓰지 않고, DEFAULT 0 으로 INSERT 무해) — 역마이그레이션 불필요. 필요 시 `ALTER TABLE daily_slot DROP COLUMN IF EXISTS entry_count` 로 제거 가능(데이터 영향: entry_count 값만 소실, slot 핵심 상태 보존).
- 데이터: closed→pending_buy 재할당으로 전이한 슬롯은 롤백 후에도 구 코드 상태 머신과 호환(pending_buy/holding/closed 는 기존 상태) — 추가 조치 불필요.
## Verification Checklist
- 배포 전:
- 단위 전체 회귀 green: BAT-025 verification 시점 `366 passed / 35 skipped / 1 deselected`(`::test_sandbox_token_and_submit` 은 LIVE `.env` KIS URL 환경 의존으로 deselect — 본 릴리즈 무관). BAT-024 단위 `353 passed`.
- `ruff check trex_core tests` All passed.
- 마이그레이션 008 idempotent 재적용 + 컬럼 계약 가드(`EXPECTED_SLOT_FIELDS`↔information_schema, entry_count 포함) green — test DSN 환경(`TREX_POSTGRESQL_TEST_DSN`)에서 확인(라이브 runtime DSN 은 `pg_clean` TRUNCATE 가 파괴적이라 미사용).
- 배포 후:
- 데몬 RUNNING + 비-degraded, 백그라운드 스레드 기동(`trex-preallocation` 포함).
- trading_date == KST 당일, 슬롯 preallocation 정상, daily_slot `entry_count` 컬럼 존재.
- `trex-core enter <pf> --forced-close-at <ISO8601> --yes` → admitted + 슬롯 forced_close_at 설정. 잘못된 형식 사전 거부.
- closed 슬롯 재할당 e2e: idle 소진 상황에서 buy 가 closed 슬롯으로 admit(`entry_count` +1), 선택이 최저 진입 우선·결정적인지 확인.
## Pre-Release Run Evidence (operator-facing smoke)
confirm-release 전 "운영자가 쓰는 프로그램이 실제로 동작한다"는 run-the-app 증거. 단위 테스트(366 passed)와 별개로, 실 데몬(RabbitMQ + 실 PostgreSQL) 기동 + `trex-core` CLI 구동으로 REL-005 의 두 신규 동작(BAT-024 `enter --forced-close-at`, BAT-025 closed 슬롯 재할당)을 검증했다. (`run-trex-core-daemon` 프로젝트 skill 경로 사용.)
- 환경: paper 브로커 + **테스트 PostgreSQL `trex_core_test`**(운영 DB 비사용 — `pg_clean` TRUNCATE 파괴성 회피) + 테스트 RabbitMQ, portfolio-hub **stub**(`pf-005930`/`pf-000660`), `TREX_TOTAL_SLOTS=1`(idle 1개만 두어 재할당 강제), instance_key `rel005smoke`(운영/기존 데몬과 큐 격리), 리비전 = 작업 트리(`941d324` + dcy-019 미커밋 변경).
- **마이그레이션 008 실 PG 적용**: `apply_migrations` 가 001~008 적용 후 `daily_slot.entry_count` 컬럼 존재 확인(실 PostgreSQL round-trip, additive·멱등). 즉, 앞서 단위에서 이월했던 PG 게이트 마이그레이션이 실 DB 에서 실제로 검증됨.
- 데몬 기동: `trex-core-daemon --config /tmp/rel005smoke.toml` → RabbitMQ 토폴로지 선언(`trex.intent.req.rel005smoke`/`trex.snapshot.req.rel005smoke`), 2026-06-09 idle 슬롯 1개(`entry_count=0`) preallocation, 에러 로그 없음, 종료 시 `Normal shutdown`.
- **BAT-024 (`enter --forced-close-at`) — PASS**:
- `trex-core enter pf-005930 --capital 1000000 --strategy-id STRAT-A --forced-close-at "2026-06-09T15:00:00+09:00" --yes --json` → `{"status":"admitted","slot_id":"4fc104cf-…"}`. DB 슬롯: `status=holding symbol=005930 entry_count=1 forced_close_at=2026-06-09 06:00:00+00`(=15:00 KST 정확 변환).
- `--forced-close-at "not-a-time"` → 제출 전 거부 `{"error":"invalid --forced-close-at (expected ISO8601): 'not-a-time'"}`, exit=1 (RPC 미발생 — 사전 검증).
- **BAT-025 (slot entry_count + closed 직접 재할당) — PASS**:
- 진입 시 `entry_count` 0→1 증가 확인(위 BAT-024 슬롯).
- `trex-core exit 4fc104cf… --yes` → `pending_sell` → `closed`(entry_count=1 보존, symbol 005930 audit 유지).
- idle 슬롯 0개 상태에서 **다른** 포트폴리오 진입 `trex-core enter pf-000660 --capital 500000 --strategy-id STRAT-B --yes` → `{"status":"admitted","slot_id":"4fc104cf-…"}` — **이전과 동일한 물리 슬롯**으로 admit(이전엔 `no_idle_slot` 거부됐을 상황). closed→pending_buy 직접 재할당 실증.
- 재할당 후 DB: `status=holding symbol=000660 entry_count=2 forced_close_at=NULL strategy_id=STRAT-B` — clean-slate(005930→000660 교체, 이전 forced_close_at 클리어)·entry_count 누적(1→2) 확인.
- 전이 로그(slot 4fc104cf): `idle→pending_buy → pending_buy→holding → holding→pending_sell → pending_sell→closed → closed→pending_buy → pending_buy→holding` — dcy-019 신규 전이 `closed→pending_buy` 가 실 데몬에서 발화.
- 운영자 RPC 정합: `trex-core slots --json` 가 재할당 슬롯(symbol 000660, STRAT-B, holding) 응답. `ledger_outbox` 3행(buy/sell/buy) 모두 `status=completed`.
- **한계(운영 배포 검증으로 이월)**: 실계좌(LIVE KIS) 주문은 미검증(paper 한정). portfolio-hub 는 stub(실 #8 연동은 본 릴리즈 변경 범위 아님 — 슬롯 도메인/CLI). LIVE full-stack 은 아래 Release Execution Evidence(실 배포)에서 확인한다.
## Release Execution Evidence
- Release Status Transition Target: released
### Deployment Facts
- Executed At:
- <배포 시작 시각>
- Completed At:
- <배포 완료 시각>
- Environment:
- <배포 대상 환경>
- Operator:
- <배포 수행자>
- Revision or Artifact:
- <커밋 SHA 또는 배포 artifact 식별자>
- Commands or Procedure:
- <실행 명령 또는 배포 절차 요약>
### Profile-Specific Checks
- app-service:
- Health Endpoint:
- <snapshot RPC degraded=false 확인 결과와 시각>
- Startup Log Fields:
- <app, event, command, run_id 확인 결과>
- Request Log Fields:
- <buy intent / forced_close / reallocation 전이 로그 확인>
- Smoke or Manual Verification:
- <enter --forced-close-at + closed 재할당 e2e 결과>
### Rollback Outcome
- Rollback Required:
- <yes 또는 no>
- Rollback Trigger:
- <발생 시 원인, 없으면 none>
- Rollback Action:
- <수행한 조치, 없으면 none>
### Release Decision Evidence
- Released Decision:
- <released 전환 여부>
- Rationale:
- <released 전환 판단 근거>
- Recorded By:
- <기록자>
- Recorded At:
- <기록 시각>
## Feedback Handoff
- Observation Summary:
- <운영 관찰 사실 요약 또는 없음>
- Discovery Input:
- <후속 discovery 후보>
- REQ Input:
- <후속 req 후보>
- Change Request Input:
- `daily_symbol_entries`(심볼 재진입 카운터) 제거 — production 소비자 없음 + 심볼 재진입 제한 미사용 확정(dcy-019). REQ-002 change-req 트랙(별도 batch-lite) — REL-005 범위 밖.