docs(batch): draft-batch BAT-028 (dcy-020 BAT-β reconcile+admission) + design 확정

REQ-058(universe reconcile)+REQ-059(자산 admission FCFS/M)+REQ-060(max_active_slots).
design 확정(사용자 결정):
- D1: config 게이트 이중경로 (max_active_slots 설정 시 자산중심, 미설정 시 legacy pool;
  운영 데몬은 cutover 전까지 pool 무중단)
- D2: "활성"(M 캡) = holding+pending_buy+pending_sell
- D3: 대량소멸 가드 = N→0 + 급감(50%↓) 경고
batches/index BAT-028(standard, planning) 등록.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 11:04:57 +09:00
co-authored by Claude Opus 4.8
parent 017b8185e0
commit 8bd9495c4b
6 changed files with 149 additions and 0 deletions
@@ -0,0 +1,46 @@
# BAT-028 Design
- Status: confirmed (D1/D2/D3 확정 — 2026-06-10, 사용자 결정)
- Batch: BAT-028
## 설계 목표
- 슬롯 lifecycle 을 universe reconcile 로, admission 을 자산 중심 FCFS+M 캡으로 전환하되, **라이브 데몬 무중단**을 위해 config 게이트로 도입하고 legacy pool 경로를 보존한다(안 B).
## 핵심 결정 — config 게이트 이중 경로 (권장)
- `DaemonInstanceConfig.max_active_slots: Optional[int]`.
- **설정됨 → 자산 중심 모드**: 슬롯 생성=universe reconcile, admission=자산 슬롯 활성화+M 캡.
- **미설정 → legacy pool 모드**: 기존 preallocation + find_allocatable_slot (현행 그대로).
- 운영 데몬은 cutover(BAT-δ) 전까지 `max_active_slots` 미설정 → **legacy 유지, 무중단**. 자산 중심은 테스트/단계 전환에서 활성.
- 대안(전면 switch)은 라이브 admission 을 한 번에 바꿔 위험 → 안 B에서 비채택.
## REQ-058 reconcile
- repo 계약 추가: `reconcile_universe(instance_key, trading_date, universe_portfolio_ids: set[str]) -> ReconcileResult`.
- universe 에 있고 슬롯 없음(instance_key, portfolio_id) → 자산 슬롯 생성(inactive, portfolio_id NOT NULL, 멱등).
- 슬롯 있고 universe 에서 빠짐 + status==inactive(idle/closed 별칭) + retired_at IS NULL → `retired_at=now` (soft-retire).
- 빠짐 + holding/pending → **tombstone**(retired_at 미설정, 유지; 신규 활성 후보 제외는 admission/조회에서 retired_at·universe 기준).
- 호출: `SnapshotScheduler.run_once()` 의 `store.swap(items)` 직후, **자산 중심 모드일 때만**. degrade(get_portfolios 실패) → reconcile 건너뜀. **대량소멸 가드**: `len(universe) == 0` 또는 급감(예: 직전 대비 50%↓) 시 retire 보류 + 경고(생성은 진행).
- 단일 writer: reconcile 쓰기는 인스턴스 스코프 advisory lock 안에서.
## REQ-059 admission (자산 중심 분기)
- `IntentService._admit_buy`: 모드 분기.
- 자산 중심: `repo.find_slot_by_portfolio(instance_key, trading_date, portfolio_id)` 로 그 자산의 슬롯을 잡는다.
- 슬롯 없음(universe 밖) → reject `unknown_portfolio`.
- 슬롯이 이미 활성(pending_buy/holding/pending_sell) → reject `already_active`(멱등).
- 활성 수 계산 `count_active(instance_key, trading_date)` ≥ M → reject `capacity_full`(무치환).
- else inactive→pending_buy 활성화(인스턴스 lock + 재시도). over-cap(M 축소)도 같은 경로로 신규만 차단.
- legacy: 기존 find_allocatable_slot 경로.
- **"활성(active)" 정의(M 캡 대상)**: `holding + pending_buy + pending_sell`(= inactive/error 아닌 포지션 점유 상태). pending_sell 은 청산 진행 중이나 아직 포지션 보유 → 캡에 포함. (체크포인트 D2)
## REQ-060 config
- `max_active_slots` per-instance TOML 로드(REQ-048 계열). 자산 중심 모드에서 admission 이 M 으로 사용. legacy 모드는 무시.
## 테스트 설계
- reconcile: 생성 멱등 / 비활성 soft-retire / 보유·pending tombstone / degrade·대량소멸 보류 (in-memory + PG).
- admission(자산 중심): 활성화 / capacity_full / already_active / over-cap / 동시성 경합 (인스턴스 lock).
- config: max_active_slots 로드 / 모드 게이트.
- legacy 회귀: max_active_slots 미설정 시 기존 pool admission/preallocation 무파손(기존 테스트 green).
## 체크포인트 결정 (2026-06-10, 사용자 확정)
1. **D1 = config 게이트 이중 경로.** `max_active_slots` 설정 시 자산 중심, 미설정 시 legacy pool. 운영 무중단(cutover 전 pool 유지).
2. **D2 = "활성"(M 캡) = `holding + pending_buy + pending_sell`** (inactive/error 아닌 포지션 점유 상태 전부).
3. **D3 = N→0 + 급감 경고.** universe 가 0이면 retire 보류; 직전 대비 급감(50%↓)도 retire 보류 + 경고(생성은 진행).
@@ -0,0 +1,16 @@
# BAT-028 Implementation
- Status: not-started
- Batch: BAT-028
## 구현 요약
- (run-batch-delivery 진입 시 작성) design.md 체크포인트(D1/D2/D3) 확정 후 채운다.
## 변경 내역
- (대기)
## 테스트
- (대기)
## 검증 실행
- (대기)
@@ -0,0 +1,32 @@
# BAT-028: universe reconcile + 활성 capacity admission (dcy-020 BAT-β)
- Status: planning
- Profile: standard
- Source Discovery: dcy-020
- Included REQ: REQ-058 (universe reconcile lifecycle), REQ-059 (활성 capacity admission FCFS·M·over-cap), REQ-060 (max_active_slots per-instance TOML)
## Summary
- dcy-020 의 **2번째 구현 배치(BAT-β)**. 슬롯 lifecycle 을 일일 preallocation 에서 **universe reconcile**(자산 1:1 생성/soft-retire/tombstone)로, buy admission 을 pool(find_allocatable_slot)에서 **자산 중심 FCFS + 동시 활성 M 캡**으로 전환한다. M 은 per-instance TOML(`max_active_slots`).
- **안 B 핵심(라이브 안전)**: 자산 중심 경로를 **config 게이트(`max_active_slots` 설정 시 활성)**로 도입하고, 미설정 시 기존 pool 동작 유지. 운영 데몬은 cutover(BAT-δ) 전까지 pool 모드로 무중단 — 신규 경로는 테스트/단계 전환용.
## Scope
- In Scope:
- REQ-060: `DaemonInstanceConfig.max_active_slots`(per-instance TOML) — config 게이트 신호
- REQ-058: `reconcile_slots(universe)` — 자산 1:1 생성(멱등)·비활성 soft-retire·보유/pending tombstone·degrade/대량소멸 보류. 스냅샷 refresh 경로에 결합
- REQ-059: 자산 중심 admission — 자산 슬롯 활성화(inactive→pending_buy), 활성(holding+pending) M 캡·`capacity_full` 거부·무치환·over-cap·already_active, 인스턴스 스코프 lock 경합
- runtime-flows.md 부분 갱신(REQ-065 부분)
- Out of Scope:
- 기존 pool 경로 제거(legacy 유지 — 제거는 BAT-δ cutover)
- intent 자산 키 통일·sell-by-portfolio_id (REQ-061 → BAT-γ)
- 수량/불일치(REQ-062), 운영자 뷰(REQ-063) → BAT-γ
- 라이브 cutover·컬럼 drop·literal inactive 전면화 (REQ-064 → BAT-δ)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Source discovery 단일(dcy-020). BAT-027(토대) 후속, BAT-γ/δ 선행.
- **핵심 리스크**: admission/슬롯 생성은 **라이브 데몬 동작**이라, config 게이트로 pool 모드 기본 유지(운영 무중단)하고 자산 중심은 신규 경로로 도입 — design 에서 게이트 경계 확정 후 구현(design 체크포인트).
@@ -0,0 +1,41 @@
# BAT-028 Planning
- Status: confirmed
- Batch: BAT-028
- Profile: standard
## Included REQ
- REQ-060 (Configuration, Approved): `max_active_slots` per-instance TOML — 자산 중심 모드 게이트 + M 값.
- REQ-058 (Integration, Approved): universe reconcile lifecycle(생성/soft-retire/tombstone, degrade 보류).
- REQ-059 (Interface, Approved): 자산 중심 admission(FCFS·M캡·무치환·over-cap·already_active).
## Delivery Plan
1. REQ-060: `DaemonInstanceConfig.max_active_slots: Optional[int]`(config/*.toml). 설정 시 자산 중심 모드, 미설정 시 legacy pool. `instance.example.toml`/`config/.env.example` 주석.
2. REQ-058: `SlotRepository.reconcile_universe(instance_key, trading_date, universe_portfolio_ids)` + in-memory/PG 구현 — 신규 portfolio_id → 자산 슬롯 생성(멱등, inactive, portfolio_id NOT NULL), universe 에서 빠진 비활성 → retired_at set, 보유/pending → tombstone(retired_at 보류). degrade/대량소멸(N→0) 가드는 호출측(scheduler). `SnapshotScheduler.run_once` 의 swap 직후 reconcile 훅(자산 중심 모드일 때만).
3. REQ-059: `IntentService._admit_buy` 자산 중심 분기 — 자산 중심 모드면 `find_slot_by_portfolio(instance_key, trading_date, portfolio_id)` 로 그 자산 슬롯을 잡아 활성화, 활성(holding+pending) 수 < M 이면 inactive→pending_buy, ==M 이면 `capacity_full`, 이미 활성이면 `already_active`. 무치환·over-cap(신규만 차단). legacy 모드면 기존 find_allocatable_slot 경로 유지.
4. daemon 배선: 자산 중심 모드면 preallocation 스레드 대신 reconcile(스냅샷 경로), admission 모드 플래그 전달.
5. runtime-flows.md 부분 갱신(reconcile/admission 흐름, 모드 게이트).
6. 단위(reconcile/admission/capacity/over-cap) + 회귀 + ruff (+ test DSN reconcile).
## Dependencies
- Internal: BAT-027(영구 슬롯 스키마·instance_key·inactive 상태머신·인스턴스 lock) 선행 완료. dcy-005/BAT-008 SnapshotScheduler·universe read. dcy-014/BAT-018 preallocation(legacy 유지).
- External: portfolio-hub universe(get_portfolios) — 이미 동작.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) admission·슬롯 생성은 **라이브 데몬 동작** 변경이라, 자산 중심 신규 경로와 legacy pool 경로의 **게이트 경계**(config 기준)를 설계에서 확정해 운영 무중단을 보장해야 한다. (b) reconcile 의 tombstone/soft-retire 가 보유 포지션·safety-sell 와 얽히므로 repo 계약·degrade 가드를 못박아야 안전. (c) M 캡 카운팅(활성=holding+pending)·over-cap·동시성(인스턴스 lock)·already_active 규칙을 admission 분기에서 정확히. → design.md 확정 후 **구현 전 사용자 체크포인트**.
## Milestones
1. M1 — REQ-060 config(게이트) + 단위
2. M2 — REQ-058 reconcile(repo + scheduler 훅) + 단위/ PG
3. M3 — REQ-059 admission 자산 중심 분기 + capacity/over-cap 단위
4. M4 — daemon 배선(모드) + runtime-flows 부분 + 회귀/ruff
## Risks
- R-mode (High): legacy/자산 중심 이중 경로 혼선·라이브 회귀. 대응: config 게이트 단일 신호(max_active_slots), 기본 legacy, 신규 경로 테스트 격리.
- R-reconcile (Medium): tombstone/soft-retire 오판으로 보유 포지션 은퇴. 대응: 보유/pending tombstone·degrade 보류·대량소멸 가드, 회귀 테스트.
- R-capacity (Medium): M 캡 동시성 경합으로 초과 활성. 대응: 인스턴스 스코프 lock + 재시도, 경합 테스트.
## Notes
- 라이브 데몬은 cutover(BAT-δ) 전까지 legacy pool 모드 유지(무중단). 자산 중심 default 전환은 BAT-δ.
- 본 배치 green 기준: 단위 전체 회귀(legacy 경로 무파손) + 신규 자산 중심 단위 + test DSN reconcile.
@@ -0,0 +1,13 @@
# BAT-028 Verification
- Status: not-started
- Batch: BAT-028
## Acceptance Mapping
- (draft-batch-verification / confirm-batch-verification 단계에서 작성)
## Evidence
- (대기)
## Result
- Ready for Release: false (구현 전)
+1
View File
@@ -35,3 +35,4 @@
| BAT-025 | standard | release-candidate | REQ-002 (CHG-20260609-01), REQ-003 (CHG-20260609-01), REQ-006 (CHG-20260609-01), REQ-007 (CHG-20260609-01), REQ-009 (CHG-20260609-01) | dcy-019 (issue #18) | [bat-025](./bat-025_20260609_slot-entry-count-and-closed-reallocation/) | | BAT-025 | standard | release-candidate | REQ-002 (CHG-20260609-01), REQ-003 (CHG-20260609-01), REQ-006 (CHG-20260609-01), REQ-007 (CHG-20260609-01), REQ-009 (CHG-20260609-01) | dcy-019 (issue #18) | [bat-025](./bat-025_20260609_slot-entry-count-and-closed-reallocation/) |
| BAT-026 | batch-lite | release-candidate | REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01) | 없음 (trex-orb-v1 정합) | [bat-026](./bat-026_20260609_config-dir-env-and-config-path/) | | BAT-026 | batch-lite | release-candidate | REQ-048 (CHG-20260609-01), REQ-010 (CHG-20260609-01) | 없음 (trex-orb-v1 정합) | [bat-026](./bat-026_20260609_config-dir-env-and-config-path/) |
| BAT-027 | standard | planning | REQ-056, REQ-057 | dcy-020 (BAT-α) | [bat-027](./bat-027_20260610_asset-permanent-slot-foundation/) | | BAT-027 | standard | planning | REQ-056, REQ-057 | dcy-020 (BAT-α) | [bat-027](./bat-027_20260610_asset-permanent-slot-foundation/) |
| BAT-028 | standard | planning | REQ-058, REQ-059, REQ-060 | dcy-020 (BAT-β) | [bat-028](./bat-028_20260610_universe-reconcile-and-active-admission/) |