# BAT-030 Implementation - Status: done - Batch: BAT-030 - 일자: 2026-06-10 ## 구현 요약 design.md 확정 결정(D2=전부 완성 / D3=매도 qty 브로커 정본) 구현 완료. M1~M5 수행. **단, D1(데이터 보존 cutover)은 폐기** — 실 DB 점검 중 마이그레이션 009 부분 유니크 인덱스가 legacy 풀 데이터(같은 자산이 closed+재진입 holding 으로 portfolio_id 중복)와 충돌해 cutover/데몬 부팅이 모두 막히는 결함 발견. 사용자 결정으로 데이터 보존 마이그레이션을 버리고 **DB wipe + 자산 중심 재초기화**(`trex-core-reset`)로 대체(REQ-064 CHG-20260610-03). 빈 테이블에는 009 인덱스가 정상 생성되고 자산 중심에서 (instance_key,portfolio_id)는 설계상 유일. - **M1 — literal inactive 전면화 (REQ-057 잔여)**: `SlotState` 5-state(inactive/pending_buy/holding/pending_sell/error) — `IDLE`/`CLOSED` 별칭·`normalize_state` 완전 제거, 전이 12종(6 base + 4 to-error + 2 resolve). 마이그레이션 **010**: 기존 idle/closed 행 → inactive UPDATE + status CHECK 교체(drop-and-add 멱등). `execution.py` 매도 체결 `PENDING_SELL→INACTIVE`, 매수 롤백 `PENDING_BUY→INACTIVE`. `cli.py` buyable 필터 `inactive`+`universe_detached` 제외. `ops_checks.py` ledger-gap 검출을 `slot_transition_log`(reason=`sell_filled`/`sell_partial_cancel`) 기반으로 재작성(`closed` 상태 소멸 대응). - **M2 — 집행 매도 수량 브로커 정본 (REQ-062 M2(b), D3)**: dispatch 매도 qty = `broker.get_balance()` 의 해당 symbol `hldg_qty`(tick 당 1회 lazy fetch). 미러 `slot.qty` fallback 없음. 브로커 0 → 주문 미제출 + WARN + `ExecutionResult.sell_skipped_no_balance` 카운터(divergence 는 reconcile/운영자 경로로 표면화). skip 매도가 큐 head 를 막지 않도록 단건 claim(`find_pending_for_dispatch`) → **리스트 기반**(`list_undispatched_pending(instance_key, limit)`) dispatch 로 전환. - **M3 — 컬럼 drop + legacy pool 제거 (REQ-056 잔여)**: 마이그레이션 **011**: `daily_slot.entry_count`·`trading_date` DROP(IF EXISTS 멱등). 글롭 재적용 정합: 001 의 `(trading_date,status)` 인덱스 → `(status)` 교체, 008(entry_count ADD)은 no-op 화(매 boot 컬럼 부활 방지). `Slot` 모델/`EXPECTED_SLOT_FIELDS`/`_SLOT_COLUMNS`/`_row_to_slot` 정합(19 컬럼). repo 계약 개편: `pre_allocate_slots`/`find_idle_slot`/`find_allocatable_slot`/`increment_symbol_entry`/`get_symbol_entry_count` 제거, `list_slots(instance_key)`/`count_slots_by_status(instance_key)`/`reconcile_universe(instance_key, universe)` 로 전환, 집행 큐(`list_undispatched_pending`/`list_dispatched_pending`)도 **instance 스코프**(공유 DB 의 두 데몬이 서로의 주문을 집행하지 않음 — REQ-056). `preallocation_scheduler.py` 삭제 + 데몬 7 스레드. admission 은 자산 중심 단일 경로(slot_id 정본/portfolio_id dual-accept; `max_active_slots` 미설정 = 무캡 — design §5: 운영 TOML 로 M 부여). `BuyIntent.trading_date`·intent/이벤트/슬롯 뷰 payload 의 `trading_date` 키 제거. capacity 를 `SlotCapacity(inactive/holding/pending/active/available/total/scarcity)` 자산 중심 재정의(available = max(0, M−active)). `daily_symbol_entries` 코드 경로 제거(테이블 잔존, baseline 에 폐기 명시). config 에서 `total_slots`/`daily_preallocation_time` 제거. SnapshotScheduler reconcile 은 slot_repo 주입 시 항상 on. - **M4 — DB 재초기화 도구 (REQ-064, CHG-20260610-03)**: `trex_core/runtime/reset.py` + console script `trex-core-reset`(기본 dry-run, `--yes` 적용). 절차: 대상 DB 확인 → slot-core 7개 테이블 중 존재하는 것 행수 집계(dry-run 은 여기서 보고만) → `--yes` 면 `TRUNCATE ... RESTART IDENTITY CASCADE` → `apply_migrations`(빈 테이블이라 009 유니크 인덱스 정상 생성·010 inactive CHECK·011 컬럼 drop) → `daily_slot` 컬럼 == EXPECTED_SLOT_FIELDS 검증. 파괴적·**DB 전역**(모든 instance). 데이터 보존(보유 carry-over)은 하지 않음 — 재초기화 후 universe reconcile 이 자산 슬롯 생성, 보유는 `reconcile_core_holdings`(REQ-062)로 운영자 재편입. (구 `cutover.py`·`trex-core-cutover`·`test_cutover_rehearsal.py` 삭제.) - **M5 — baseline 마감 (REQ-065)**: data-model/runtime-flows/project-structure/interface-contract 4문서 자산 중심 최종 갱신(daily_symbol_entries·dcy-014 preallocation·풀 admission 폐기 확정, slot_id 계약·5-state·M 캡·브로커 정본 반영). `config/instance.example.toml` 에 `max_active_slots` 안내. `.claude/skills/run-trex-core-daemon`(SKILL.md+smoke.py) 자산 중심으로 갱신. ## 변경 내역 (Changed Files) - trex_core/domain/slot/: state.py(5-state) · models.py(컬럼 drop+SlotCapacity 재정의) · service.py(preallocate/카운터 제거, INACTIVE 리셋=점유 초기화+자산 정체성 보존, capacity 자산 중심) · repository.py · in_memory_repository.py(+add_slot 테스트 헬퍼) · __init__.py - trex_core/adapters/persistence/: postgresql.py · ops_checks.py(재작성) · sql/001(인덱스 교체)·008(no-op 화)·**010_literal_inactive_status.sql**(신규)·**011_drop_pool_model_columns.sql**(신규) - trex_core/application/: intents.py(단일 자산 중심 admission) · execution.py(M2+inactive 전이+instance 스코프) · messaging_handlers.py · portfolio.py · safety_sell.py · killswitch.py · snapshot_scheduler.py · **preallocation_scheduler.py 삭제** - trex_core/runtime/: daemon.py(7 스레드) · config.py · cli.py · **reset.py(신규)** · ~~cutover.py(삭제)~~ - pyproject.toml: `trex-core-reset` console script (구 `trex-core-cutover` 제거) - docs: data-model.md · runtime-flows.md · project-structure.md · interface-contract.md (자산 중심 마감) - config/instance.example.toml · .claude/skills/run-trex-core-daemon/{SKILL.md,smoke.py} - tests: 도메인/애플리케이션/어댑터/런타임 전면 정합(test_state·test_service 재작성, test_slot_entry_count·test_buy_admission_reallocation·test_preallocation_scheduler 삭제, test_execution 에 M2 3건 신설, **tests/runtime/test_reset.py 신규 5건(PG-gated)**, test_postgresql_repository 자산 중심 재작성+dispatch 인스턴스 격리, test_ops_checks 재작성) ## 테스트 - 단위 전체: **367 passed** (PG/RabbitMQ/KIS unset 시 40 skipped), ruff clean - PG-gated(trex_core_test): **26 passed** — 마이그레이션 010/011 round-trip + 멱등 재적용, daily_slot 컬럼 == EXPECTED_SLOT_FIELDS 드리프트 가드(컬럼 drop 후 정합), reconcile ON CONFLICT, dispatch 인스턴스 격리, ops_checks 재작성, **reset 5건**(wipe+자산 중심 스키마 착지·dry-run 무변경·멱등·wipe 후 즉시 admission 가능·slot-core 테이블 커버리지) - RabbitMQ-gated: 21 passed (실 브로커) - 통합(PG+RabbitMQ enabled): **411 passed, 1 skipped**(KIS sandbox — 기존 환경 이슈, 본 배치 무관) ## 검증 실행 (run-the-app, 자산 중심 e2e) test DB(trex_core_test) + 실 RabbitMQ + paper broker + stub universe(2 자산) + `instance_key=e2etest`, `max_active_slots=1` 로 데몬 기동: 1. boot 직후 universe reconcile `{'created': 2}` — 영구 슬롯 자동 생성 2. snapshot RPC: slots=2(inactive), `available_slots=1`(M−active) 3. **buy by slot_id** admitted(REQ-061) → 4. 동일 intent_id 중복 → identical 응답(전이 1회) 5. M=1 에서 2번째 자산 buy → `capacity_full`(REQ-059) → 6. paper fill → holding, available=0 7. 동일 슬롯 재매수 → `already_active` → 8. sell by slot_id → 체결 → **inactive**(qty=None, portfolio_id 보존, available=1) 9. **동일 영구 슬롯 재활성**(re-entry) admitted - DB 최종: `('e2etest','pf-000660','inactive',None)`, `('e2etest','pf-005930','holding',3.0)`; transition_log reason {buy_intent×2, buy_filled×2, discretionary_sell×1, sell_filled×1}; ledger_outbox completed×3; 데몬 로그 ERROR/Traceback 0, Normal shutdown. ## Remaining Risks / Blockers - **실 DB 재초기화 미실행(의도)** — 운영자가 `trex-core-reset --config --yes` 실행(파괴적·DB 전역). 사전 백업 필수. 재초기화라 롤백 = 백업 복원. - 데이터 보존 cutover 폐기로 **기존 보유 포지션은 재초기화 시 소실** — 재초기화 후 브로커 보유는 universe reconcile + `reconcile_core_holdings`(REQ-062)에서 unmanaged 로 표면화되어 운영자가 재편입. 실거래 보유가 있는 DB 라면 재편입 절차를 release 노트에 포함. - 마이그레이션 009 부분 유니크 인덱스는 legacy 풀 중복 데이터에 적용 불가(이 결함이 cutover→reset 전환의 직접 원인). 자산 중심 DB(신규/재초기화 후)에서는 정상. - 전략 모듈(trex-orb) 계약 전환: buy `slot_id` 정본(snapshot slots[] round-trip), `trading_date` 키 소멸, 이벤트 payload 변경 — portfolio_id dual-accept 로 무중단이나 전략 측 전환 권고(REL 노트 대상). - `max_active_slots` 미설정 = 무캡 — 운영 TOML 에 반드시 설정(배포 체크리스트, design §5).