chore(governance): remove legacy StagePilot artifacts

This commit is contained in:
Hermes Agent
2026-08-08 21:36:27 +09:00
parent 536c534a50
commit 2e5f59e09c
601 changed files with 0 additions and 55555 deletions
@@ -1,30 +0,0 @@
# BAT-001 Design
- Status: confirmed
- Batch: BAT-001
## Architecture Summary
- trex-core 를 `trex_core` import 패키지(배포명 `trex-core`)로 부트스트랩하고, 첫 도메인으로 slot 순수 도메인을 `trex_core/domain/slot/` 에 둔다. 도메인은 저장소를 `SlotRepository` Protocol 로 추상화하고 구현체(InMemorySlotRepository)를 주입받는 storage-agnostic 구조다. 인프라(향후 PostgreSQL/CLI/config)는 도메인을 의존할 수 있으나 그 역은 금지한다.
## Changed Areas
- Docs: docs/project-structure.md, docs/runtime-flows.md (REQ-004 정합), docs/data-model.md·docs/interface-contract.md (계약 기준 참조)
- Modules: pyproject.toml, trex_core/__init__.py, trex_core/domain/__init__.py, trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py
- Interfaces: 공개 API = SlotState, Slot, SlotCapacity, SlotTransitionRecord, SlotRepository, SlotService, 예외 4종, is_allowed_transition, SLOT_ADVISORY_LOCK_ID, ALLOWED_TRANSITIONS
## Key Decisions
1. 네임스페이스 `trex_core.domain.slot`(dcy-001 OQ-1). 모듈 간 import 를 모두 `trex_core.domain.slot.*` 로 통일하고 `app.domain.slot` 잔존을 0으로 만든다.
2. 컬럼 계약 박제 = 계약 테스트(dcy-001 OQ-2). 도메인에 `EXPECTED_SLOT_FIELDS` 표준 필드 집합을 선언하고 `test_contract.py` 가 `{f.name for f in dataclasses.fields(Slot)}` 와의 일치를 단언. 타입 단언은 하지 않고 타입 매핑은 문서(부록 A/data-model)에 둔다.
3. 공개 API 는 인프라 심볼(`PostgreSQLSlotRepository`, `check_slot_domain_schema_drift`)을 노출하지 않는다 — 애초 `__init__` 이 이를 export 하지 않으므로 docstring 정리만 수행.
4. `SLOT_ADVISORY_LOCK_ID` 는 순수 도메인에 잔류(현행 유지). 영속화 계층 신설 시 재배치 여부는 후속 반복(dcy-001 OQ-3 인접) 판단.
## Edge Cases
- `EXPECTED_SLOT_FIELDS` 와 `Slot` 필드 불일치 시 계약 테스트가 실패해야 한다(음성 케이스 의도적 검증).
- `closed→idle`/`pending_buy→idle` 복구 시 점유 필드 초기화 + 재진입 카운트 +1 불변식 보존 확인(이식 테스트 범위).
- in-memory repo 의 threading.Lock 이 advisory lock 의미를 모사하므로, 동시성 관련 테스트도 DB 없이 성립.
## Architecture Impact
- both (project-structure + runtime-flows)
## Reference Doc Update Plan
- project-structure.md: planned 트리·모듈 책임의 `(planned)` 표기를 실제 `trex_core/` 구조로 갱신(REQ-004). 네임스페이스는 이미 `trex_core/` 로 정정 완료, 잔여는 실재 반영.
- runtime-flows.md: in-memory 기반 도메인 흐름(preallocate/transit)이 실제 코드와 일치하는지 확인하고 "미구현/planned" 성격 표기를 정정(REQ-004).
@@ -1,35 +0,0 @@
# BAT-001 Implementation
- Status: completed
- Batch: BAT-001
## Plan Summary
- REQ-001 부트스트랩 → REQ-002 slot 순수 도메인 이식 → REQ-003 테스트·계약 가드 → REQ-004 baseline 정합을 순차 구현했다. trex-orbit `app/domain/slot/` 의 순수 6개 모듈 + `__init__` 을 `trex_core.domain.slot` 로 복사하고 import 를 전면 치환, 테스트 2종 이식 + 계약 테스트 신설.
## Changed Files
- 신규(REQ-001): `pyproject.toml`, `trex_core/__init__.py`, `trex_core/domain/__init__.py`, `tests/__init__.py`, `tests/domain/__init__.py`, `tests/domain/slot/__init__.py`
- 신규(REQ-002): `trex_core/domain/slot/{__init__,state,models,errors,repository,service,in_memory_repository}.py`
- import 경로 `app.domain.slot.*` → `trex_core.domain.slot.*` 전면 치환, `__init__` docstring 순수 도메인 기준 정리(인프라 export 없음 확인)
- 신규(REQ-003): `tests/domain/slot/{test_state,test_service,test_contract}.py`, `models.py` 에 `EXPECTED_SLOT_FIELDS` 선언
- 갱신(REQ-004): `docs/project-structure.md`, `docs/runtime-flows.md` planned→실제 정합
## Execution Log
1. `pyproject.toml`(name=trex-core, requires-python>=3.12, setuptools, dev extras=pytest·ruff) + 패키지 레이아웃 생성.
2. 순수 6개 모듈 + `__init__` 이식, import 치환, `EXPECTED_SLOT_FIELDS` 추가.
3. `test_state.py`·`test_service.py` 이식(미사용 import 정리, `t1.start()/join()` 멀티스테이트먼트 분리), `test_contract.py` 신설.
4. `.venv` 생성 후 `pip install -e .[dev]` (PEP 668 외부관리 환경이라 venv 사용).
5. `pytest`, `ruff check .`, 공개 API import, import 스윕, 계약 음성 케이스 실행.
6. `docs/project-structure.md`·`docs/runtime-flows.md` 정합(REQ-004).
## Validation
- `pytest -q`: **56 passed** (test_state 18 + test_service 35 + test_contract 3, 근사).
- `ruff check .`: **All checks passed**.
- 공개 API import: 15개 심볼 전부 import 성공(`SlotState`...`EXPECTED_SLOT_FIELDS`).
- import 스윕: `trex_core/domain/slot/*.py` 에 stdlib + `trex_core.domain.slot` 외 외부 의존 **0**.
- 공개 `__all__`: `PostgreSQL`/`schema` 인프라 심볼 노출 **0**.
- 계약 음성 케이스: `Slot` 에 rogue 필드 추가 시 계약 일치 단언이 False 로 떨어짐(실패 재현 확인).
## Remaining Risks
- R-1 영속화 부재(in-memory 한정) — 후속 Discovery 인계.
- R-2 DB-side 컬럼 드리프트 검사 미도입 — 영속화 REQ 인계(계약 테스트는 코드 측 가드 확보).
- 환경: 시스템 Python 3.14.4 에서 venv 로 검증(requires-python>=3.12 충족). CI 표준 인터프리터 고정은 후속.
@@ -1,32 +0,0 @@
# BAT-001: trex-core 베이스 부트스트랩 + slot 순수 도메인 시드
- Status: released
- Profile: standard
- Source Discovery: dcy-001
- Included REQ: REQ-001, REQ-002, REQ-003, REQ-004
- Excluded REQ: 없음
## Summary
- dcy-001 의 단일 가치(trex-core 베이스 수립 + slot 순수 도메인 시드)를 하나의 delivery 단위로 묶는다. 부트스트랩(REQ-001) → 도메인 이식(REQ-002) → 테스트·계약 가드(REQ-003) → baseline 정합(REQ-004)이 같은 iteration·같은 모듈 경로(`trex_core/domain/slot`)·같은 검증 흐름(`pytest`)을 공유한다.
## Scope
- In Scope:
- REQ-001: `pyproject.toml`(배포명 `trex-core`) + `trex_core/`·`trex_core/domain/` 레이아웃 + pytest/ruff 설정.
- REQ-002: 순수 6개 모듈 + `__init__` 을 `trex_core.domain.slot` 로 이식, 공개 API 인프라 심볼 없이 노출.
- REQ-003: 단위테스트(`test_state`/`test_service`) 이식 green + 계약 가드(`EXPECTED_SLOT_FIELDS` + `test_contract`).
- REQ-004: 이식 후 `docs/project-structure.md`·`docs/runtime-flows.md` planned→실제 정합.
- Out of Scope:
- PostgreSQL 구현체/ SQL DDL/ `db init`/ schema drift 감지.
- `trex slot` CLI, config 해석, coordinator 연동, archive/prune.
- trex-orbit 수정 및 trex-orbit↔trex-core 코드 수렴.
## 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-001 단일. 모든 REQ 가 dcy-001 FR-1~FR-5 에서 분해됨.
- 공통 리스크: 영속화 부재(in-memory 한정, dcy-001 R-1), 컬럼 계약 드리프트(REQ-003 계약 테스트로 완화, R-2), 이중 진실(병행, R-3).
- 네임스페이스 `trex_core.domain.slot`(dcy-001 OQ-1), 컬럼 계약 박제 = 계약 테스트(OQ-2). DB-side 드리프트 검사는 영속화 반복 인계.
@@ -1,42 +0,0 @@
# BAT-001 Planning
- Status: confirmed
- Batch: BAT-001
- Profile: standard
## Included REQ
- REQ-001 (Installation): trex-core Python 프로젝트 부트스트랩
- REQ-002 (Interface): slot 순수 도메인 이식 및 공개 API 확정
- REQ-003 (Testing): slot 도메인 단위테스트 이식 및 컬럼 계약 가드
- REQ-004 (Documentation): baseline 문서 planned→실제 정합
## Delivery Plan
- 선형 의존을 따라 순차 구현한다.
1. REQ-001: `pyproject.toml`(name=`trex-core`, requires-python>=3.12, setuptools, dev extras=pytest·ruff) + `trex_core/__init__.py`, `trex_core/domain/__init__.py` + `tests/` 스캐폴딩 + ruff/pytest 설정.
2. REQ-002: trex-orbit `app/domain/slot/` 의 순수 6개 모듈 + `__init__` 을 `trex_core/domain/slot/` 로 복사하고 모듈 간 import 를 `trex_core.domain.slot.*` 로 치환, `__init__` docstring 정리(인프라 export 없음 확인).
3. REQ-003: `tests/domain/slot/test_state.py`·`test_service.py` 이식, `trex_core/domain/slot` 에 `EXPECTED_SLOT_FIELDS` 선언, `test_contract.py` 신설(`fields(Slot)` 일치 단언).
4. REQ-004: 위 산출물 확정 후 `docs/project-structure.md`·`docs/runtime-flows.md` 의 planned 표기를 실제 구조로 정합.
## Dependencies
- Internal:
- REQ-002 는 REQ-001(패키지 레이아웃) 선행 필요.
- REQ-003 은 REQ-002(도메인 코드) 선행 필요.
- REQ-004 는 REQ-002(실제 구조 확정) 선행 필요.
- External:
- 없음(외부 서비스·DB 미사용). 개발 의존성: pytest, ruff(dev extras).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 패키지 네임스페이스(`trex_core`) 도입 + 공개 API 표면 확정 + baseline 구조/런타임 문서 정합이 함께 걸린다(구조·인터페이스 영향). planning 만으로는 import 경계·공개 API·계약 가드 위치 결정이 드리프트할 수 있어 design.md 를 같은 턴에 작성한다.
## Milestones
1. M1 — REQ-001: 부트스트랩 완료(`pip install -e .[dev]` + `pytest` 수집 성공 + `ruff check` 동작).
2. M2 — REQ-002: 도메인 이식 + 공개 API import 성공 + import 스윕 외부 의존 0.
3. M3 — REQ-003: 이식 테스트 2종 green + 계약 테스트 green(필드 변경 시 실패 재현).
4. M4 — REQ-004: baseline 문서 정합(`app/` 잔존 0, 트리/흐름 실제 일치).
## Risks
- R-1 (Medium): 영속화 부재 → in-memory repo 한정 구동. 대응: 이번 batch 목표를 "도메인 코어 시드 + 테스트 green"으로 한정, 영속화는 후속 Discovery.
- R-2 (Medium): 컬럼 계약 드리프트. 대응: REQ-003 계약 테스트(`EXPECTED_SLOT_FIELDS`)로 자동 강제, DB-side 검사 인계.
- R-3 (Low): trex-orbit·trex-core 이중 진실. 대응: 의도된 임시 비용으로 수용, 수렴은 별도 Discovery.
- R-4 (Low): import 경로 치환 누락(`app.domain.slot` 잔존). 대응: 이식 후 grep 으로 잔존 import 0 확인(REQ-002 AC).
@@ -1,46 +0,0 @@
# BAT-001 Verification
- Status: confirmed
- Batch: BAT-001
## Acceptance Mapping
- REQ-001 (Installation):
- AC1 설치/import → `pip install -e .[dev]` 성공 + `import trex_core` OK (.venv, Python 3.14.4 ≥ 3.12). **PASS**
- AC2 pytest 수집 → `pytest -q` 56 passed (수집/실행 정상). **PASS**
- AC3 pyproject 메타 → name=`trex-core`, requires-python>=3.12, setuptools, dev extras=pytest·ruff 확인. **PASS**
- AC4 ruff 동작 → `ruff check .` All checks passed. **PASS**
- REQ-002 (Interface):
- AC1 공개 API import → 15개 심볼(SlotState…EXPECTED_SLOT_FIELDS) import 성공. **PASS**
- AC2 외부 의존 0 → import 스윕 결과 stdlib + `trex_core.domain.slot` 외 import 없음. **PASS**
- AC3 인프라 심볼 비노출 → `__all__` 에 PostgreSQL/schema 심볼 0. **PASS**
- AC4 도메인 불변식 보존 → test_state(전이 14종) + test_service(advisory lock 직렬화·closed→idle 재진입+1·race 감지) 통과. **PASS**
- REQ-003 (Testing):
- AC1 이식 테스트 green → test_state + test_service 통과(56 passed 에 포함). **PASS**
- AC2 계약 테스트 green → test_contract `fields(Slot)==EXPECTED_SLOT_FIELDS` 통과. **PASS**
- AC3 필드 변경 시 실패 재현 → rogue 필드 추가 시 계약 일치 False 확인(live negative case). **PASS**
- AC4 타입 단언 배제 → test_contract 는 이름/존재만 단언, 타입 매핑은 부록 A/data-model 문서. **PASS**
- REQ-004 (Documentation):
- AC1 project-structure 정합 → `(planned)` 제거, 실제 `trex_core/domain/slot` 트리·모듈 책임 반영. **PASS**
- AC2 runtime-flows 정합 → preallocate/transit 흐름이 실제 코드 기반임을 Change Log 반영, 영속화 미구현 표기 유지(정확). **PASS**
- AC3 네임스페이스 일관 → `docs/project-structure.md`·`docs/runtime-flows.md` 에 `app/` 잔존 0(Change Log 정정 이력 1건 제외). **PASS**
## Evidence
- Test:
- `pytest -q` → `56 passed in 0.06s` (test_state / test_service / test_contract).
- 계약 음성 케이스: `dataclasses.make_dataclass` 로 rogue 필드 추가 → 계약 일치 단언 False.
- Manual:
- import 스윕 grep: `trex_core/domain/slot/*.py` 외부/인프라 import 0.
- `__all__` 점검: 인프라 심볼 0.
- baseline 문서 `app/` 잔존 grep: 정정 Change Log 1건만 매칭.
- Logs:
- run-batch-implementation 단계 실행 로그(implementation.md Validation 절) 참조.
## Baseline Sync Evidence (Architecture Impact = both)
- docs/project-structure.md: planned 트리·모듈 책임을 실제 `trex_core/` 구조로 갱신 + Change Log 기록(REQ-004).
- docs/runtime-flows.md: 도메인 흐름이 실제 코드 기반임을 반영 + Change Log 기록(REQ-004).
- design.md `Reference Doc Update Plan` 의 두 항목 모두 반영 완료.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음 (모든 포함 REQ AC 가 evidence 와 연결됨. 잔여 리스크 R-1/R-2 는 후속 영속화 반복 인계 사항으로 비차단)
@@ -1,39 +0,0 @@
# BAT-002 Design
- Status: confirmed
- Batch: BAT-002
## Architecture Summary
- onion 3-레이어로 구성한다. **domain**(`trex_core.domain.slot`, BAT-001, 무의존) ← **application**(intent admission/슬롯 전이 유스케이스, SlotService 호출) ← **adapters**(FastAPI HTTP 입구 + psycopg PostgreSQL SlotRepository) + **runtime**(데몬 엔트리포인트/uvicorn/lifecycle). 코어 데몬이 `daily_slot` 단일 writer + intent_log 소유. 전략 데몬은 HTTP 클라이언트.
## Changed Areas
- Docs: docs/interface-contract.md(/v1 API), docs/runtime-flows.md(데몬·요청 흐름), docs/project-structure.md(레이어), docs/data-model.md(intent_log + 영속 테이블) — REQ-009
- Modules:
- runtime: 데몬 엔트리포인트(console_scripts), uvicorn 기동, lifecycle(startup: preallocate / shutdown: graceful), `/v1/healthz`
- adapters/api: FastAPI 앱 + `/v1` 라우터(intents buy/sell/{id}, slots, capacity) + pydantic 요청/응답 스키마
- adapters/persistence: psycopg `SlotRepository` 구현 + 마이그레이션 SQL
- application: intent 유스케이스(admission → SlotService.transit_slot + intent_log 기록, intent_id 멱등)
- Interfaces: `/v1` HTTP 6종(dcy-002 OQ-3)
## Key Decisions
1. 동기 admission+커밋: HTTP 핸들러가 단일 트랜잭션(advisory lock)에서 admission→슬롯 전이→intent_log 기록을 커밋하고 admit/reject 동기 응답(dcy-002 OQ-1). 완전 비동기 outbox 는 브로커 Discovery 로 Deferred.
2. 멱등: `intent_log.intent_id` UNIQUE + 수락 전 dedup 조회. 재전송 시 최초 decision 재응답. `GET /v1/intents/{id}` 로 재조회.
3. 단일 writer(불변식 A): `daily_slot` 쓰기는 application 의 SlotService 경로뿐. read API(REQ-008)는 쓰기 없음. 동시 intent 는 `pg_advisory_xact_lock(SLOT_ADVISORY_LOCK_ID)` 로 직렬화.
4. 스키마(dcy-002 OQ-4): 부록 A 단일 기준 trex-core 소유 마이그레이션. 테이블 = daily_slot/daily_symbol_entries/slot_transition_log/intent_log. archive/prune 제외. orbit-v2 DDL 은 참조.
5. 도메인 순수성(NFR-1): FastAPI/psycopg 의존은 adapters/runtime 에만. `trex_core.domain.slot` 은 import 스윕 0 유지(BAT-001 계약 테스트 패턴 확장).
## Edge Cases
- 동일 intent_id 재전송(네트워크 재시도) → 슬롯 중복 전이 없이 최초 decision 반환.
- 동시 buy intent 가 같은 idle 슬롯/마지막 슬롯 경합 → advisory lock 직렬화, 한쪽은 admit 한쪽은 reject(슬롯 소진).
- sell intent 의 slot_id 가 이미 holding 이 아님(이미 전이됨) → reject + 사유.
- PostgreSQL 미가용(검증 환경) → 통합테스트 evidence blocker 로 분리(코드/단위는 별도 통과).
- preallocate 재기동 → 멱등(이미 있으면 0).
## Architecture Impact
- both (project-structure + runtime-flows). interface-contract/data-model 도 신규 계약/테이블로 갱신.
## Reference Doc Update Plan
- project-structure.md: domain/application/adapters/runtime 레이어와 신규 모듈 트리 반영(REQ-009).
- runtime-flows.md: 데몬 lifecycle 흐름 + intent 요청 흐름(admission→전이→intent_log) 추가(REQ-009).
- interface-contract.md: `/v1` 6종 입력/출력/에러/멱등 계약 추가(REQ-009).
- data-model.md: intent_log 엔티티 + 영속 테이블 계약 반영(REQ-009).
@@ -1,38 +0,0 @@
# BAT-002 Implementation
- Status: completed
- Batch: BAT-002
## Plan Summary
- REQ-005 런타임 → REQ-006 영속화 → REQ-007 intent API → REQ-008 조회 API → REQ-009 baseline 정합을 순차 구현. onion 4-레이어(domain/application/adapters/runtime). 도메인(BAT-001)은 무수정 재사용.
## Changed Files
- pyproject.toml: fastapi/uvicorn/psycopg 의존성 + dev httpx + console_scripts `trex-core-daemon`.
- application: trex_core/application/{ports.py(IntentStore), intents.py(IntentService/BuyIntent/SellIntent/IntentDecision)}
- adapters/persistence: postgresql.py(PostgreSQLSlotRepository + PostgreSQLIntentStore), in_memory_intent_store.py, migrate.py, sql/001_create_slot_core_tables.sql
- adapters/api: schemas.py(pydantic), app.py(create_app, /v1 라우터)
- runtime: config.py(DaemonConfig), daemon.py(build_app + lifespan migrate/preallocate + uvicorn main)
- tests: tests/adapters/api/test_intent_api.py, tests/application/test_intent_concurrency.py, tests/adapters/persistence/test_postgresql_repository.py, tests/adapters/persistence/test_migration_and_drift.py, tests/conftest.py
- docs(REQ-009): interface-contract.md, runtime-flows.md, project-structure.md, data-model.md
## Execution Log
1. SQL 마이그레이션(daily_slot[부록 A 16컬럼]/daily_symbol_entries/slot_transition_log/intent_log) + migrate 러너.
2. PostgreSQL SlotRepository(연결-당-호출, pg_advisory_xact_lock) + PostgreSQLIntentStore(intent_id UNIQUE dedup gate).
3. IntentService: dedup(try_insert) → admission(find idle / fetch holding) → SlotService.transit_slot(advisory lock) → finalize intent_log. last-idle 경합은 SlotStateMismatchError 재시도.
4. FastAPI create_app(/v1 healthz·intents/buy·sell·{id}·slots·capacity), DI 로 in-memory/PG 양쪽 구동.
5. 데몬 엔트리포인트: lifespan 에서 migrate + 거래일 preallocate(공개 엔드포인트 아님).
6. 테스트 작성 + 로컬 PostgreSQL 18 기동 DB(trex_core_test)로 통합 실행.
## Validation
- `pytest`: **74 passed** (PostgreSQL DSN 설정 시) / **66 passed + 8 skipped** (DSN 미설정 시 PG 통합 graceful skip).
- `ruff check .`: **All checks passed**.
- 도메인 순수성(NFR-1): `trex_core/domain/` 에 fastapi/uvicorn/psycopg/starlette import **0**(grep 확인).
- 멱등(NFR-3): API/PG 양쪽에서 동일 intent_id 재전송 시 동일 decision·슬롯 중복 전이 없음(테스트 + 데몬 스모크).
- 단일 writer(NFR-2): 20-thread 동시 buy → 정확히 idle 수(5)만 admit, 슬롯 중복 사용 0.
- 드리프트(REQ-006): `EXPECTED_SLOT_FIELDS` ↔ `daily_slot` information_schema 일치 + 음성 케이스 검출.
- 데몬 E2E 스모크: lifespan migrate+preallocate(5 idle) → buy admit→pending_buy → 멱등 resubmit 동일 slot_id → capacity 갱신 → intent lookup, PostgreSQL 영속 확인.
## Remaining Risks
- intent 비원자성 미세 창: dedup insert 와 transit 가 별 트랜잭션이라, 둘 사이 크래시 시 intent_log 가 'processing' 으로 남을 수 있음(슬롯 중복 전이는 UNIQUE gate 로 차단). 완전 원자/outbox 는 브로커 Discovery 인계.
- admission 은 슬라이스 1 기준 슬롯 가용성 중심(orbit REQ-028 다중 게이트는 범위 밖).
- 연결-당-호출 repo 는 고부하 시 풀링 필요(후속 최적화).
@@ -1,32 +0,0 @@
# BAT-002: trex-core 슬롯 코어 데몬화 + HTTP intent API (슬라이스 1)
- Status: released
- Profile: standard
- Source Discovery: dcy-002
- Included REQ: REQ-005, REQ-006, REQ-007, REQ-008, REQ-009
- Excluded REQ: 없음
## Summary
- dcy-002 의 슬라이스 1(데몬 + HTTP API + PostgreSQL 영속화 + 멱등 intent)을 하나의 delivery 단위로 묶는다. BAT-001 의 순수 도메인을 안쪽 코어로 두고 그 바깥에 런타임(REQ-005)·영속화(REQ-006)·API(REQ-007/008)·문서정합(REQ-009)을 얹는다.
## Scope
- In Scope:
- REQ-005: 데몬 런타임 + lifecycle(기동/정상종료/`GET /v1/healthz`) + 거래일 preallocate(lifecycle 내부) + FastAPI/uvicorn/psycopg 의존성·entrypoint.
- REQ-006: PostgreSQL `SlotRepository` + 신규 마이그레이션(daily_slot/daily_symbol_entries/slot_transition_log/intent_log) + 드리프트 테스트.
- REQ-007: `POST /v1/intents/buy|sell` + 동기 admission/전이 + intent_id 멱등 + `intent_log` + `GET /v1/intents/{id}`.
- REQ-008: `GET /v1/slots?status=holding` + `GET /v1/capacity` (read-only).
- REQ-009: interface-contract/runtime-flows/project-structure/data-model 정합.
- Out of Scope (후속 Discovery):
- 브로커 단일 세션/주문 집행(불변식 B), safety floor(forced_close/EOD/손절 backstop), 완전 비동기 outbox, 매수·매도 전략 데몬, 인증/인가 고도화·수평 확장.
## 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-002 단일. 모든 REQ 가 dcy-002 FR-1~FR-6 에서 분해됨.
- 확정 설계(dcy-002 OQ): 동기 커밋+intent_log(OQ-1) / FastAPI+uvicorn(OQ-2) / `/v1` 엔드포인트 6종(OQ-3) / 부록 A 기준 신규 스키마(OQ-4).
- 핵심 리스크: 실제 PostgreSQL·HTTP·신규 의존 도입(검증 환경 필요), 도메인 순수성 유지(어댑터 격리), 단일 writer 불변식.
- 검증 환경: PostgreSQL 인스턴스 필요(통합테스트). 미가용 시 일부 evidence 가 blocker 로 남을 수 있음(verification 에서 분리 판단).
@@ -1,47 +0,0 @@
# BAT-002 Planning
- Status: confirmed
- Batch: BAT-002
- Profile: standard
## Included REQ
- REQ-005 (Installation): 슬롯 코어 데몬 런타임 부트스트랩
- REQ-006 (Data): PostgreSQL SlotRepository 영속화 + 스키마
- REQ-007 (Interface): intent 제출 HTTP API + 멱등
- REQ-008 (Interface): 슬롯 read-only 조회 HTTP API
- REQ-009 (Documentation): 데몬/API/영속화 baseline 정합
## Delivery Plan
- 의존 순서대로 구현한다(기반 → API → 문서).
1. REQ-005: pyproject 에 FastAPI/uvicorn/psycopg + console_scripts; 데몬 lifecycle(기동/shutdown/`/v1/healthz`); preallocate 를 lifecycle 내부로.
2. REQ-006: psycopg 기반 `SlotRepository` 구현 + trex-core 소유 마이그레이션(daily_slot[부록 A 16컬럼]/daily_symbol_entries/slot_transition_log/intent_log) + `pg_advisory_xact_lock` + 드리프트 테스트.
3. REQ-007: FastAPI 라우터 `POST /v1/intents/buy|sell` → 애플리케이션 서비스가 `SlotService` 로 admission/전이(advisory lock 직렬화) + intent_log 동기 기록 + intent_id 멱등 + `GET /v1/intents/{id}`.
4. REQ-008: `GET /v1/slots?status=holding` + `GET /v1/capacity` read-only.
5. REQ-009: baseline 4문서 정합.
## Dependencies
- Internal:
- REQ-006·REQ-007·REQ-008 은 REQ-005(런타임 기반) 선행.
- REQ-007·REQ-008 은 REQ-006(영속화, 특히 intent_log) 선행.
- REQ-009 는 REQ-005~008 구현 완료 후.
- 안쪽 코어는 BAT-001 의 `trex_core.domain.slot`(완성) 재사용.
- External:
- PostgreSQL 인스턴스(통합테스트/런타임). psycopg, FastAPI, uvicorn(dev/runtime 의존).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 3개 레이어(데몬 런타임 + FastAPI API + psycopg 영속화)와 onion 경계(domain/application/adapters), 단일 writer 강제 지점, intent_log/멱등 트랜잭션 경계를 구현 전에 고정해야 한다. 구조/인터페이스/런타임 영향이 모두 있어 design.md 를 같은 턴에 작성한다.
## Milestones
1. M1 — REQ-005: 데몬 기동 + `/v1/healthz` 200 + graceful shutdown.
2. M2 — REQ-006: 마이그레이션 적용 + PostgreSQL repo 로 도메인 테스트 통과 + 드리프트 테스트 green.
3. M3 — REQ-007: buy/sell intent 전이 + 멱등 재전송 + 동시성 단일 writer 통합테스트.
4. M4 — REQ-008: holding 스냅샷 + capacity 조회.
5. M5 — REQ-009: baseline 4문서 정합.
## Risks
- R-1 (Medium-High): 실제 PostgreSQL/HTTP/신규 의존 도입 — 검증 환경(PG 인스턴스) 필요. 대응: 통합테스트용 PG 준비; 미가용 시 해당 evidence 를 verification 의 blocker/missing-evidence 로 분리(코드/단위는 별도 통과).
- R-2 (Medium): 도메인 순수성 오염(웹/psycopg). 대응: domain/application/adapters 레이어 분리 + import 스윕 가드(NFR-1) — `trex_core.domain.slot` import 0 유지.
- R-3 (Medium): 단일 writer 불변식 붕괴(API 핸들러 경유 다중 writer). 대응: 모든 쓰기를 `SlotService`+advisory lock 단일 경로로 강제, 동시성 통합테스트로 검증(NFR-2).
- R-4 (Medium): intent 멱등/내구성 결함. 대응: intent_id dedup + intent_log 동기 커밋 + 재전송/재조회 테스트(NFR-3).
- R-5 (Low): 컬럼 계약 드리프트. 대응: `EXPECTED_SLOT_FIELDS`↔information_schema 드리프트 테스트(REQ-006).
@@ -1,53 +0,0 @@
# BAT-002 Verification
- Status: confirmed
- Batch: BAT-002
## Acceptance Mapping
- REQ-005 (daemon runtime):
- AC1 기동/`/v1/healthz` 200 → 데몬 E2E 스모크(TestClient lifespan) healthz `{status:ok}`. **PASS**
- AC2 graceful shutdown → lifespan 컨텍스트 정상 종료(연결-당-호출, 풀 없음). **PASS**
- AC3 lifecycle preallocate(멱등, 비공개) → lifespan 에서 preallocate 5 idle 확인, 재기동 멱등. **PASS**
- AC4 deps/entrypoint → pyproject 에 fastapi/uvicorn/psycopg + console_scripts `trex-core-daemon` 설치 확인. **PASS**
- REQ-006 (PostgreSQL persistence):
- AC1 마이그레이션·컬럼 부록 A 일치 → `test_migrations_create_expected_tables` + `test_daily_slot_columns_match_expected_contract`. **PASS**
- AC2 PG repo 로 도메인 테스트 통과 → `test_postgresql_repository.py`(preallocate/전이/재진입+1). **PASS**
- AC3 드리프트 테스트 → EXPECTED_SLOT_FIELDS↔information_schema 일치 + 음성 케이스(`test_drift_detected_when_contract_extended`). **PASS**
- AC4 advisory lock → `pg_advisory_xact_lock` 구현 + 동시성 테스트로 직렬화 확인. **PASS**
- REQ-007 (intent API + 멱등):
- AC1 buy 전이 → `test_buy_intent_admits_and_transitions` + PG `test_buy_intent_persists_transition`. **PASS**
- AC2 sell 전이/거부 → `test_sell_intent_admits_from_holding` / `test_sell_intent_requires_holding`. **PASS**
- AC3 멱등 재전송(NFR-3) → `test_buy_intent_idempotent_resubmit`(API) + `test_buy_intent_idempotent_on_pg`(PG) + 데몬 스모크(동일 slot_id). **PASS**
- AC4 동시성 단일 writer(NFR-2) → `test_concurrent_buys_admit_exactly_idle_count`(20스레드→정확히 5 admit) + `test_duplicate_intent_id_admits_once_under_concurrency`. **PASS**
- AC5 intent 재조회 → `test_get_intent_roundtrip`(`GET /v1/intents/{id}`). **PASS**
- REQ-008 (read-only API):
- AC1/2 holding 스냅샷·필터 → `test_list_slots_and_capacity`(status/trading_date 필터). **PASS**
- AC3 capacity → 동 테스트(idle/holding/pending + scarcity_ratio). **PASS**
- REQ-009 (baseline 정합):
- AC1 interface-contract /v1 계약 → docs/interface-contract.md 에 6 엔드포인트 + 불변식 추가. **PASS**
- AC2 runtime-flows/project-structure 레이어 → 데몬/intent 흐름 + application/adapters/runtime 레이어 반영. **PASS**
- AC3 data-model intent_log → docs/data-model.md 에 intent_log 엔티티 + PostgreSQL 백엔드 반영. **PASS**
## Evidence
- Test:
- `pytest`: **74 passed** (TREX_POSTGRESQL_TEST_DSN=trex_core_test) / **66 passed + 8 skipped** (DSN 미설정 시 PG graceful skip).
- 동시성: 20-thread buy → 5 admit / 15 reject, pending=5·idle=0.
- 드리프트: EXPECTED_SLOT_FIELDS == daily_slot 컬럼; 음성 케이스 검출.
- Manual:
- 도메인 import 스윕: `trex_core/domain/` 에 fastapi/uvicorn/psycopg/starlette import 0(NFR-1).
- 데몬 E2E 스모크(PostgreSQL 18): healthz→capacity(5 idle)→buy admit(pending_buy)→멱등 resubmit 동일 slot_id→capacity(4 idle/1 pending)→intent lookup; `daily_slot`·`intent_log` 영속 확인.
- `ruff check .`: All checks passed.
- Logs:
- implementation.md Validation 절 + 본 batch 작업 로그.
## Baseline Sync Evidence (Architecture Impact = both)
- interface-contract.md: HTTP `/v1` 6 엔드포인트 계약 + 멱등/단일writer 불변식 추가(REQ-009).
- runtime-flows.md: 데몬 lifecycle + 매수/매도 intent 흐름 추가, Primary Runtime=daemon+library.
- project-structure.md: application/adapters/runtime 레이어 + entrypoint 반영.
- data-model.md: PostgreSQL 백엔드 + intent_log 엔티티 반영.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 모든 포함 REQ AC 가 evidence 와 연결됨(로컬 PostgreSQL 18 로 통합 evidence 확보).
- 비차단 잔여: intent dedup/transit 비원자성 미세 창(슬롯 중복 전이는 UNIQUE gate 로 차단), 연결 풀링 미도입 — 후속 인계.
@@ -1,28 +0,0 @@
# BAT-003 Implementation
- Status: completed
- Batch: BAT-003
## Plan Summary
- REQ-010: 데몬 기동 시 `.env` best-effort 로드(OS env 우선). minor-change batch-lite.
## Changed Files
- pyproject.toml: `python-dotenv>=1.0` 의존성 추가.
- trex_core/runtime/config.py: `_load_dotenv()`(best-effort, `override=False`, `find_dotenv(usecwd=True)`) 추가 + `from_env()` 시작부 호출.
- .gitignore: `/.env` 추가.
- .env.example: 키 목록 참고용 신규(추적 대상).
- tests/runtime/test_config_dotenv.py: 로드/우선순위/부재/필수누락 4 테스트.
## Execution Log
1. config 에 best-effort dotenv 로더 추가(미설치/부재 시 no-op).
2. pyproject 의존성 + .gitignore + .env.example.
3. 단위테스트 작성, `pip install -e .` 후 실행.
## Validation
- `pytest tests/runtime/test_config_dotenv.py`: 4 passed.
- 전체 `pytest`: **78 passed** (기존 74 + 신규 4), ruff clean.
- gitignore: `.env` IGNORED, `.env.example` trackable.
- 스모크: `./.env` 만으로 `DaemonConfig.from_env()` 가 dsn/total/port 로드(OS env 미export) 확인.
## Remaining Risks
- 없음(저위험). dotenv 미설치/.env 부재 시 graceful no-op로 회귀 없음(AC-3 테스트).
@@ -1,28 +0,0 @@
# BAT-003: 데몬 설정 .env 로딩 (minor-change)
- Status: released
- Profile: batch-lite
- Source Discovery: dcy-002 (REQ-010 은 데몬 설정 편의 개선)
- Included REQ: REQ-010
- Excluded REQ: 없음
## Summary
- REQ-010 단일 저위험 Configuration 요구사항을 minor-change fast path 로 처리한다. 데몬 기동 시 `.env` 를 로드(실 OS env 우선)하고, `.env` 는 gitignore, `.env.example` 제공.
## Scope
- In Scope:
- python-dotenv 의존성 추가, `DaemonConfig.from_env()` 에 `.env` best-effort 로드.
- `.gitignore` 에 `.env` 추가, `.env.example` 신규.
- 설정 로드/우선순위/부재 회귀 단위테스트.
- Out of Scope:
- 다른 설정 방식(TOML/CLI/시크릿 매니저) — 본 변경과 무관.
## Documents
- Planning: [planning.md](./planning.md)
- Design: none (batch-lite, no architecture impact)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- minor-change: 구조/인터페이스/런타임 흐름 변경 없음(기동 시 env 소스 추가뿐). design 불요.
- dotenv 미설치/.env 부재 시 graceful no-op → 회귀 없음.
@@ -1,30 +0,0 @@
# BAT-003 Planning
- Status: confirmed
- Batch: BAT-003
- Profile: batch-lite
## Included REQ
- REQ-010 (Configuration): 데몬 설정 .env 파일 로딩
## Delivery Plan
1. pyproject.toml 에 `python-dotenv` 의존성 추가.
2. `trex_core/runtime/config.py` 의 `DaemonConfig.from_env()` 시작부에서 best-effort `load_dotenv()`(미설치/부재 시 no-op, OS env override 안 함).
3. `.gitignore` 에 `.env` 추가, `.env.example` 신규(키 목록 참고용).
4. `tests/runtime/test_config_dotenv.py`: .env 로드 / OS env 우선 / .env 부재 회귀.
## Dependencies
- Internal: REQ-005(DaemonConfig) 위 확장.
- External: python-dotenv.
## Design Gate
- Design Required Before Implementation: no
- Reason: 구조/인터페이스/런타임 흐름 변경 없음. 기동 시 환경변수 소스(.env)만 추가하는 국소 설정 변경이라 batch-lite + design 생략(minor-change).
## Milestones
1. M1 — config 에 .env 로딩 + deps + gitignore/.env.example.
2. M2 — 단위테스트 green(로드/우선순위/부재).
## Risks
- R-1 (Low): .env 가 실 OS env 를 덮어쓰면 운영 사고. 대응: `load_dotenv(override=False)` 기본(실 env 우선)으로 AC-2 검증.
- R-2 (Low): dotenv 미설치 환경 회귀. 대응: best-effort import(no-op) + AC-3 회귀 테스트.
@@ -1,23 +0,0 @@
# BAT-003 Verification
- Status: confirmed
- Batch: BAT-003
## Acceptance Mapping
- REQ-010:
- AC1 .env 로드 → `test_loads_values_from_dotenv` + 스모크(dsn/total/port). **PASS**
- AC2 OS env 우선 → `test_os_env_takes_precedence_over_dotenv`(override=False). **PASS**
- AC3 .env 부재 회귀 → `test_no_dotenv_uses_os_env_only` + `test_missing_dsn_raises`. **PASS**
- AC4 gitignore/.env.example → `git check-ignore`: `.env` ignored, `.env.example` trackable. **PASS**
## Evidence
- Test: `pytest tests/runtime/test_config_dotenv.py` 4 passed; 전체 78 passed; ruff clean.
- Manual: `git check-ignore .env` → ignored, `.env.example` → trackable. `./.env` 단독 로드 스모크.
## no architecture impact confirmed
- batch-lite, planning Design Gate=no. 구조/인터페이스/런타임 흐름 변경 없음(기동 시 env 소스 추가뿐). baseline 문서 영향 없음 → 동기화 evidence 불요.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음.
@@ -1,38 +0,0 @@
# BAT-004 Design
- Status: confirmed
- Batch: BAT-004
## Architecture Summary
- BAT-002 onion(domain/application/adapters/runtime) 위에 집행을 추가한다. **application** 에 `BrokerPort`(submit/cancel/poll_fill)와 execution 유스케이스, **adapters/broker** 에 paper/stub + KIS 어댑터, **runtime** 에 백그라운드 execution 루프(데몬 lifecycle 에서 기동/종료). 코어가 단일 브로커 세션(불변식 B) + 단일 슬롯 writer(불변식 A)를 모두 소유. intent API 는 pending 전이까지만, 집행은 루프가 비동기.
## Changed Areas
- Docs: runtime-flows(집행/체결/reconcile 흐름), project-structure(broker/execution 레이어), interface-contract(BrokerPort 경계), data-model(broker_order_id=실행큐/멱등) — REQ-015
- Modules:
- application: `BrokerPort`+`FillStatus`, execution 유스케이스(claim/dispatch/poll/transition/reconcile)
- adapters/broker: paper(결정적 async 더블), kis(KIS Open API)
- runtime: execution 루프(lifespan 백그라운드), config(KIS 자격증명/base URL/게이팅/폴링 주기)
- Interfaces: `BrokerPort`(내부 포트) — 외부 HTTP API(/v1)는 불변(집행은 백그라운드)
## Key Decisions
1. 옵션 A 실행큐(OQ-3): pending 슬롯 + `broker_order_id IS NULL` 을 `FOR UPDATE SKIP LOCKED` 로 claim, 송신 후 broker_order_id 기록(멱등 가드). 별도 outbox 테이블 없음.
2. 통일 비동기 체결(OQ-2): `submit_order→accepted`, `poll_fill→filled/partial/rejected`. paper 도 동기 단축 없이 같은 계약(결정적 더블), KIS 는 체결조회 폴링.
3. 별도 execution 루프(OQ-4): 데몬 프로세스 내 백그라운드(claim→submit→poll→transition→reconcile). 단일 인스턴스=단일 writer/세션.
4. 단일 writer(불변식 A): 집행 루프도 슬롯 전이는 SlotService(advisory lock)로만. 불변식 B: 주문/취소는 BrokerPort 단일 세션만.
5. KIS 안전(R-3): 기본 모의투자, 실계좌는 config 게이팅. 자격증명 .env(REQ-010 패턴). 도메인 순수성(NFR-2): broker/HTTP 의존은 adapters/runtime 에만.
## Edge Cases
- 송신 후 broker_order_id 기록 전 크래시 → reconciliation 으로 브로커 상태 대조(중복 주문 방지).
- 부분체결 → 슬라이스는 완전체결 기준 전이(부분체결 누적/분할주문 본격화는 옵션 B 승격 후속).
- KIS 토큰 만료/레이트리밋 → 어댑터 내 갱신/백오프, 실패 시 슬롯 전이 안 함(다음 틱 재시도).
- 거부/취소 → pending_buy→idle 롤백(점유 초기화), pending_sell→holding 복귀(매도 거부).
- KIS 자격증명 미가용 → 통합테스트 skip, paper 경로는 완전 동작.
## Architecture Impact
- both (project-structure + runtime-flows). interface-contract(BrokerPort)·data-model(broker_order_id 의미) 도 갱신.
## Reference Doc Update Plan
- runtime-flows.md: 집행 루프(claim→submit→poll→transition)·reconcile·KIS 흐름 추가(REQ-015).
- project-structure.md: application(BrokerPort/execution)·adapters/broker(paper,kis)·runtime(execution 루프) 레이어 반영.
- interface-contract.md: `BrokerPort` 내부 포트 계약(submit/cancel/poll_fill) + KIS 어댑터 노트.
- data-model.md: `broker_order_id` 를 실행큐 claim 술어 + 멱등 가드로 쓰는 의미 명시.
@@ -1,26 +0,0 @@
# BAT-004 Implementation
- Status: draft
- Batch: BAT-004
## Plan Summary
- run-batch-implementation 단계에서 채운다. 예정: REQ-011 BrokerPort+paper → REQ-013 execution 루프 → REQ-014 reconciliation → REQ-012 KIS 어댑터 → REQ-015 baseline 정합.
## Changed Files
- (구현 시 기록) 예정:
- trex_core/application/broker_port.py (BrokerPort, FillStatus)
- trex_core/application/execution.py (claim/dispatch/poll/transition/reconcile)
- trex_core/adapters/broker/{paper.py, kis.py}
- trex_core/runtime/(execution 루프 lifespan 통합), config.py(KIS 자격증명/게이팅/폴링 주기)
- pyproject.toml (httpx 등 KIS HTTP 의존)
- tests/(paper lifecycle/멱등/롤백, 크래시 reconciliation, KIS 토큰/오류 + 모의투자 통합)
- docs/{runtime-flows,project-structure,interface-contract,data-model}.md (REQ-015)
## Execution Log
1. (구현 시 기록)
## Validation
- (구현 시 기록) 예정: `pytest`(paper lifecycle/멱등/롤백/reconciliation, KIS 단위) + KIS 모의투자 통합(가용 시) + `ruff` + import 스윕(도메인 broker 의존 0).
## Remaining Risks
- (구현 시 갱신) planning R-1~R-5 참조. 특히 R-3(KIS 실 API)·R-5(자격증명 가용성).
@@ -1,31 +0,0 @@
# BAT-004: 브로커 집행 + KIS 어댑터 + durable 핸드오프
- Status: released
- Profile: standard
- Source Discovery: dcy-003
- Included REQ: REQ-011, REQ-012, REQ-013, REQ-014, REQ-015
- Excluded REQ: 없음
## Summary
- dcy-003 슬라이스(브로커 단일 세션·주문 집행·비동기 체결·durable 핸드오프)를 하나의 delivery 단위로 묶는다. BAT-002 의 슬롯 코어 데몬 위에 BrokerPort + paper/KIS 어댑터 + 백그라운드 execution 루프 + reconciliation 을 얹어 intent→실제 주문을 잇는다.
## Scope
- In Scope:
- REQ-011: `BrokerPort`(submit/cancel/poll_fill) + paper/stub async 더블(불변식 B 경계).
- REQ-012: 실제 KIS Open API 어댑터(토큰/레이트리밋/모의투자 검증, 실계좌 게이팅).
- REQ-013: 백그라운드 execution 루프 — pending claim(옵션 A) → 주문 송신 → poll 체결 → holding/closed 전이 + 거부 롤백(idle).
- REQ-014: durable 핸드오프 + 재기동 reconciliation(broker_order_id 멱등, 중복 주문 0).
- REQ-015: runtime-flows/project-structure/interface-contract/data-model 정합.
- Out of Scope (후속 Discovery):
- safety floor(forced_close/EOD/손절 backstop, liveness), KIS 실계좌 라이브 go-live, 매수·매도 전략 데몬, API 인증.
## 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-003 단일. 확정 결정: KIS 어댑터까지(OQ-1) / 통일 비동기 체결(OQ-2) / 옵션 A 실행큐(OQ-3) / 별도 execution 루프(OQ-4).
- 핵심 리스크: 실거래(R-1 중복주문/R-2 KIS 비동기체결/R-5 KIS 실 API) — paper 더블 + KIS 모의투자 + reconciliation + 실계좌 게이팅으로 완화(dcy-003 confirm 수용).
- 검증 환경: KIS 모의투자 자격증명(.env, REQ-010 패턴) 가용 시 REQ-012 통합 검증, 미가용 시 skip 하고 paper 기반(REQ-011/013/014) 완전 검증 — verification 에서 분리 판단.
@@ -1,42 +0,0 @@
# BAT-004 Planning
- Status: confirmed
- Batch: BAT-004
- Profile: standard
## Included REQ
- REQ-011 (Integration): BrokerPort + paper/stub async 더블
- REQ-012 (Integration): KIS Open API 브로커 어댑터
- REQ-013 (Integration): 주문 집행 루프 (claim·송신·체결·롤백)
- REQ-014 (Exception): 주문 핸드오프 durability + reconciliation
- REQ-015 (Documentation): 브로커/집행 baseline 문서 정합
## Delivery Plan
- 의존 순서대로 구현한다.
1. REQ-011: `trex_core/application/broker_port.py`(BrokerPort + FillStatus) + `adapters/broker/paper.py`(결정적 async 더블).
2. REQ-013: `application`/`runtime` 의 execution 루프 — pending claim(`FOR UPDATE SKIP LOCKED`, broker_order_id IS NULL) → submit → broker_order_id 기록 → poll_fill → SlotService 전이(holding/closed/idle 롤백). paper 로 lifecycle 검증.
3. REQ-014: 루프에 reconcile 단계 + 재기동 복구(broker_order_id 있는 pending 을 브로커 상태와 대조). 크래시 주입 테스트.
4. REQ-012: `adapters/broker/kis.py`(토큰/레이트리밋/모의투자 base URL/게이팅) + config. KIS 모의투자 검증(가용 시).
5. REQ-015: baseline 4문서 정합.
## Dependencies
- Internal: REQ-013/014 는 REQ-011(BrokerPort) 선행. REQ-012 는 REQ-011 포트 구현. 모두 BAT-002 슬롯 코어 데몬 위.
- External: KIS Open API(모의투자) + 자격증명(.env), HTTP 클라이언트(httpx). PostgreSQL(기존).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 BrokerPort 경계(불변식 B) + 백그라운드 execution 루프(동시성·단일 writer 유지) + 비동기 체결/reconciliation + KIS 외부 연동이 구조/인터페이스/런타임에 모두 영향. 구현 전 경계·루프 lifecycle·멱등/복구 규칙을 design.md 에 고정.
## Milestones
1. M1 — REQ-011: BrokerPort + paper 더블(결정적 fill 시나리오).
2. M2 — REQ-013: paper 로 매수/매도 lifecycle(claim→송신→체결→holding/closed) + 멱등 + 거부 롤백.
3. M3 — REQ-014: 크래시 주입 reconciliation(송신 전/후) + 중복 주문 0.
4. M4 — REQ-012: KIS 어댑터(토큰/주문/체결조회) + 모의투자 검증(가용 시) + 게이팅.
5. M5 — REQ-015: baseline 4문서 정합.
## Risks
- R-1 (High): 실주문 중복/유실. 대응: broker_order_id 멱등 가드 + reconciliation(REQ-014), paper 크래시 시나리오 테스트.
- R-2 (High): KIS 비동기 체결 복잡(부분/지연/거부). 대응: 통일 submit→poll 계약, paper async 더블로 결정적 재현 + KIS 모의투자.
- R-3 (High): KIS 실 API(토큰/레이트리밋/실자금). 대응: 모의투자만 검증, 실계좌 config 게이팅(Out of Scope), 자격증명 .env.
- R-4 (Medium): 단일 writer/도메인 순수성 침식. 대응: 슬롯 쓰기 SlotService 단일 경로, broker adapters 격리, import 스윕.
- R-5 (Medium): KIS 모의투자 자격증명 미가용 시 REQ-012 검증 공백. 대응: paper 기반(011/013/014) 완전 검증, KIS 통합은 skip+blocker 분리(verification).
@@ -1,48 +0,0 @@
# BAT-004 Verification
- Status: confirmed
- Batch: BAT-004
## Acceptance Mapping
- REQ-011 (BrokerPort + paper double):
- AC1 submit→poll 비동기(동기 단축 없음) → `test_submit_does_not_synchronously_fill`. **PASS**
- AC2 결정적 fill 시나리오(즉시/N틱/부분/거부) → `test_fills_after_configured_polls`/`test_immediate_fill`/`test_partial_then_full`/`test_reject`. **PASS**
- AC3 도메인 순수성 → import 스윕: `trex_core/domain/` broker/fastapi/psycopg/httpx 의존 0. **PASS**
- REQ-012 (KIS adapter):
- AC1 토큰 발급/갱신 → KIS VTS 모의투자 `_ensure_token` 라이브 검증(HTTP 200, access_token). **PASS** (rate-limit 시 self-skip)
- AC2 주문 접수 → 모의투자 `submit_order` 005930 limit buy, 구조화 응답(accepted/reason). **PASS**
- AC3 체결 조회 → `poll_fill`(inquire-daily-ccld) 구현; 모의투자 미체결 시 accepted 반환. **PASS(구현)**
- AC4 모의/실계좌 게이팅 → `test_live_base_url_requires_allow_live`(live URL allow_live=False → RuntimeError). **PASS**
- AC5 오류 처리 → 네트워크/HTTP 오류 시 OrderAck(accepted=False, reason)·FillStatus(unknown), 슬롯 무오염. **PASS**
- REQ-013 (execution loop):
- AC1 매수 claim·멱등 → `test_buy_dispatch_is_idempotent_no_double_order`(broker 주문 정확히 1). **PASS** (NFR-2)
- AC2 매수 체결→holding → `test_buy_lifecycle_to_holding`(avg_price/qty). **PASS**
- AC3 거부 롤백→idle → `test_buy_reject_rolls_back_to_idle`. **PASS**
- AC4 매도 체결→closed → `test_sell_lifecycle_to_closed_bumps_entry_count`. **PASS**
- AC5 단일 writer → 집행 루프도 SlotService(advisory lock)로만 전이(코드/도메인 순수성). **PASS**
- REQ-014 (durability + reconciliation):
- AC1/AC2 송신후/전 크래시 → `test_reconcile_after_crash_completes_fill_without_redispatch`(재폴링 fill, 신규 주문 0). **PASS**
- AC3 체결 수렴 → 동 테스트 holding 수렴. **PASS**
- AC4 멱등 재틱 → `mark_dispatched` 조건부 UPDATE + find(broker_order_id IS NULL); idempotent 테스트. **PASS** (NFR-3)
- REQ-015 (baseline 정합):
- AC1 runtime-flows 집행 루프/reconcile 흐름 추가. **PASS**
- AC2 project-structure broker/execution 레이어 반영. **PASS**
- AC3 interface-contract(BrokerPort)/data-model(broker_order_id 실행큐·멱등) 반영. **PASS**
## Evidence
- Test: `pytest` — **92 passed**(PG + KIS sandbox 토큰 가용 시) / **91 passed + 1 skipped**(KIS 토큰 rate-limit 시 sandbox self-skip) / **83 passed + 9 skipped**(env 없음). ruff clean.
- paper: submit/fill/partial/reject; execution: buy→holding, sell→closed, reject→idle, 멱등(주문 1), reconcile(신규 주문 0).
- KIS: 계정분해/모의 tr_id/라이브 게이팅 단위 + **KIS VTS 모의투자 토큰+주문 라이브 round-trip 통과**(이 세션 확인).
- Manual: 도메인 import 스윕 broker/web/db 의존 0. 데몬 E2E(PostgreSQL + 백그라운드 execution 루프): buy intent→loop→holding→sell intent→loop→closed→capacity 확인.
- Logs: implementation.md Validation 절 + 본 batch 작업 로그.
## Baseline Sync Evidence (Architecture Impact = both)
- runtime-flows.md: `주문 집행 루프`(dispatch/poll/reconcile) flow 추가.
- project-structure.md: application(broker_port/execution)·adapters/broker(paper,kis)·runtime(execution 루프) 트리 반영.
- interface-contract.md / data-model.md: BrokerPort 경계 + broker_order_id 실행큐·멱등 의미 Change Log 반영.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. paper 경로(REQ-011/013/014)는 완전 결정적 검증. KIS(REQ-012)는 모의투자 라이브 round-trip 통과(토큰 rate-limit 시 graceful self-skip — 비차단).
- 비차단 잔여: KIS 모의투자 체결은 시장 개장/잔고에 따라 비결정적(미체결 가능) — 완전 체결 lifecycle 은 paper 로 보장. 부분체결 누적·옵션 B outbox·실계좌 라이브 go-live·연결 풀링은 후속 인계.
@@ -1,29 +0,0 @@
# BAT-005 Implementation
- Status: completed
- Batch: BAT-005
## Plan Summary
- REQ-016: KISBroker 에 선택적 Redis 토큰 캐시(L1 인메모리 → L2 캐시 → 발급) + graceful fallback. orbit Redis 패턴 이식.
## Changed Files
- trex_core/adapters/broker/token_cache.py (신규): `TokenCache` Protocol, `RedisTokenCache`(redis.from_url, 키 `trex:broker:kis:{account_ref}:token`, JSON 토큰+만료+app_key suffix, ex=ttl, 예외 graceful), `InMemoryTokenCache`(더블/폴백).
- trex_core/adapters/broker/kis.py: `token_cache` 주입, `_ensure_token` 3-레벨(L1/L2/issue) + 발급 시 store.
- trex_core/runtime/config.py: `KISConfig.redis_url` + `TREX_KIS_REDIS_URL`.
- trex_core/runtime/daemon.py: redis_url 있으면 `RedisTokenCache` 주입.
- pyproject.toml: `redis>=5` 의존.
- tests/adapters/broker/test_token_cache.py (신규).
## Execution Log
1. TokenCache 추상화 + Redis/InMemory 구현(app_key suffix 안전, 만료 검증, 예외 graceful).
2. KISBroker _ensure_token 3-레벨 + store.
3. config/daemon wiring + pyproject redis.
4. 테스트 작성, redis 라이브러리 설치 후 실행.
## Validation
- `pytest tests/adapters/broker/test_token_cache.py`: 4 passed (캐시 히트 HTTP 0 / 인스턴스 간 공유 / graceful fallback / app_key suffix 무효화).
- 전체 `pytest`: **96 passed** (기존 92 + 신규 4), ruff clean.
- 실 Redis 서버 불요: 인메모리 캐시 더블 + httpx.post 스텁으로 결정적 검증. RedisTokenCache 의 redis.from_url 경로는 운영 시 사용(라이브러리 import OK 확인).
## Remaining Risks
- 없음(저위험). Redis 장애 시 graceful fallback(발급 진행). 실 Redis 서버 통합 검증은 운영 환경에서(라이브러리/와이어링 확인 완료).
@@ -1,28 +0,0 @@
# BAT-005: KIS 토큰 Redis 캐시 (minor-change)
- Status: released
- Profile: batch-lite
- Source Discovery: dcy-003 (REQ-012 운영 견고성 보완)
- Included REQ: REQ-016
- Excluded REQ: 없음
## Summary
- REQ-016 단일 저위험 Integration 요구사항을 minor-change 로 처리. KISBroker 에 선택적 Redis 토큰 캐시(L1 인메모리 → L2 Redis → 발급)를 더해 재기동/다중 프로세스 토큰 생존 + rate-limit 회피. orbit Redis 캐시 패턴 이식.
## Scope
- In Scope:
- `TokenCache` 추상화 + `RedisTokenCache`(redis.from_url, 키 `trex:broker:kis:{account_ref}:token`, app_key suffix 안전, ex=ttl) + 테스트용 인메모리 더블.
- KISBroker `_ensure_token` 3-레벨 + graceful fallback. config `TREX_KIS_REDIS_URL` + daemon wiring. pyproject redis 의존.
- 캐시 히트/공유/폴백/suffix 단위테스트.
- Out of Scope:
- 다른 캐시 백엔드(PG/파일), 토큰 외 응답 캐시.
## Documents
- Planning: [planning.md](./planning.md)
- Design: none (batch-lite, no external interface/flow change)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- minor-change: 기존 BrokerPort 뒤 토큰 캐시 계층 추가, 외부 인터페이스·슬롯 흐름 불변. design 불요.
- 실 Redis 없을 때도 graceful fallback → 단일 데몬 회귀 없음. 테스트는 인메모리 캐시 더블로 결정적 검증(실 Redis 서버 불요).
@@ -1,32 +0,0 @@
# BAT-005 Planning
- Status: confirmed
- Batch: BAT-005
- Profile: batch-lite
## Included REQ
- REQ-016 (Integration): KIS 인증 토큰 Redis 캐시
## Delivery Plan
1. `trex_core/adapters/broker/token_cache.py`: `TokenCache` Protocol(load/store) + `RedisTokenCache`(redis.from_url, JSON 토큰+만료+app_key suffix, ex=ttl, 예외 graceful) + `InMemoryTokenCache`(테스트/폴백 더블).
2. `kis.py`: `token_cache` 주입, `_ensure_token` = L1 인메모리 → L2 캐시(load, suffix 검증, 만료 확인) → 발급+store.
3. `runtime/config.py`: `TREX_KIS_REDIS_URL` → KISConfig.redis_url. `runtime/daemon.py`: redis_url 있으면 RedisTokenCache 주입.
4. `pyproject.toml`: `redis>=5` 의존.
5. `tests/adapters/broker/test_token_cache.py`: 캐시 히트(HTTP 0)/공유 재사용/graceful fallback/app_key suffix.
## Dependencies
- Internal: REQ-012 KISBroker 확장.
- External: redis 라이브러리(+ 운영 시 Redis 서버). 테스트는 인메모리 더블.
## Design Gate
- Design Required Before Implementation: no
- Reason: 기존 BrokerPort 뒤 토큰 캐시 계층 추가뿐. 외부 인터페이스/슬롯 런타임 흐름 변경 없음. batch-lite + design 생략(minor-change).
## Milestones
1. M1 — TokenCache + RedisTokenCache + KISBroker 3-레벨.
2. M2 — config/daemon wiring + pyproject.
3. M3 — 단위테스트 green(히트/공유/폴백/suffix).
## Risks
- R-1 (Low): Redis 장애가 주문 경로를 막음. 대응: 캐시 예외 graceful fallback(발급으로 진행), 캐시는 best-effort.
- R-2 (Low): 자격증명 교체 후 stale 토큰. 대응: app_key suffix 검증으로 캐시 무효화(AC-4).
@@ -1,23 +0,0 @@
# BAT-005 Verification
- Status: confirmed
- Batch: BAT-005
## Acceptance Mapping
- REQ-016:
- AC1 캐시 히트 → HTTP 발급 없음 → `test_cache_hit_avoids_issuance`(L1 drop 후 L2 히트, token call 1회 유지). **PASS**
- AC2 인스턴스 간 공유(재기동/다중프로세스) → `test_shared_cache_across_instances`(B 인스턴스 재발급 0). **PASS**
- AC3 graceful fallback → `test_graceful_fallback_when_cache_errors`(캐시 miss/오류 시 발급 진행). **PASS**
- AC4 app_key suffix 안전 → `test_app_key_suffix_mismatch_ignores_cache`(자격증명 교체 시 stale 무시 재발급). **PASS**
## Evidence
- Test: `pytest` 4 token-cache passed; 전체 96 passed; ruff clean.
- Manual: redis 라이브러리 import + RedisTokenCache wiring 확인. 실 Redis 서버 없이 인메모리 더블로 결정적 검증.
## no architecture impact confirmed
- batch-lite, planning Design Gate=no. 기존 BrokerPort 뒤 토큰 캐시 계층 추가뿐 — 외부 인터페이스/슬롯 런타임 흐름 변경 없음. baseline 문서 영향 없음.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 실 Redis 서버 통합은 운영 환경 검증 사항(라이브러리/와이어링/폴백 검증 완료, 비차단).
@@ -1,42 +0,0 @@
# BAT-006 Design
- Status: draft
- Batch: BAT-006
## Architecture Summary
- BAT-004 execution loop 위에 ledger write-back 레이어를 추가한다. **application** 에 `LedgerPort`와 ledger outbox use case, **adapters/ledger** 에 in-memory/stub 및 portfolio-hub HTTP 어댑터, **runtime** 에 ledger retry worker/config 를 둔다. execution fill 전이는 슬롯 상태 전이를 먼저 보장하고 ledger 기록은 outbox 로 적재해 eventually-consistent 하게 처리한다.
## Changed Areas
- Docs: runtime-flows(체결→ledger outbox→portfolio-hub retry/conflict 흐름), interface-contract(`LedgerPort`, `FillStatus` 확장, portfolio-hub endpoint), project-structure(ledger adapter/outbox 위치), data-model(ledger trade fields, outbox work item, correlation_id).
- Modules:
- application: `LedgerPort`, ledger exceptions, ledger outbox command/service, execution fill hook.
- adapters/ledger: in-memory/stub ledger, portfolio-hub HTTP adapter.
- adapters/broker: paper/KIS `FillStatus.execution_id` 합성, fee 계산 운반.
- runtime: portfolio-hub config, ledger retry loop lifecycle.
- persistence: ledger outbox work item store/schema.
- Interfaces: `LedgerPort.record_trade(...)`, `FillStatus.execution_id/fee`, portfolio-hub `POST /v1/portfolios/{portfolio_id}/ledger/trades` (body `quantity`/`fee_amount`/`tax_amount`, 성공 201). (정정: REQ-017 CHG-20260606-01 — 구 `POST /v1/assets/{asset_id}/...`, body `qty`/`fee`/`tax` 폐기)
## Key Decisions
1. `asset_id == portfolio_id`: 슬롯의 `portfolio_id` 를 ledger path param 으로 직접 사용한다. 매핑 조회나 asset master 동기화는 후속 범위다.
2. outbox 우선: fill 전이 경로는 ledger 직접 HTTP write 대신 durable outbox work item 을 적재한다. 슬롯 전이는 ledger write 성공과 분리한다.
3. 멱등 키: `correlation_id = {asset_id}:{order_id}:{execution_id}` 로 로컬 dedup 하고 portfolio-hub 409 는 `conflict_blocked` 로 분류한다.
4. outcome 어휘: `completed`, `duplicate_completed`, `retryable_failure`, `conflict_blocked` 네 가지로 worker 결과를 고정한다.
5. 도메인 순수성: `trex_core/domain` 은 portfolio-hub, HTTP, ledger adapter 를 import 하지 않는다. LedgerPort 는 application 경계, HTTP adapter 는 adapters 경계에 둔다.
6. 단일 기록 주체: 전략/도메인이 아니라 execution fill path 만 ledger outbox item 을 생성한다.
## Edge Cases
- 동일 `execution_id` work item 이 2회 생성/처리됨 → correlation_id 로 dedup, 원장 중복 0, outcome `duplicate_completed`.
- portfolio-hub 일시 실패/timeout → `retryable_failure`, 다음 worker tick 에 재시도, 슬롯 상태는 이미 전이된 상태 유지.
- portfolio-hub 409 conflict → `conflict_blocked`, 자동 무한 재시도 중단, 운영자 해소 대상으로 남김.
- `portfolio_id` 없는 체결 → 정의된 정책에 따라 ledger 기록 skip 또는 blocked outcome 으로 처리하되 슬롯 전이는 정상 수행.
- KIS 체결 데이터 부족 → 결정적 execution_id 합성 규칙으로 보강하고, fee 는 BrokerPort 계산, tax 는 0.
- retry worker 재시작 → persisted outbox 상태를 기준으로 미완료 item 만 재처리.
## Architecture Impact
- both (project-structure + runtime-flows). interface-contract(`LedgerPort`, `FillStatus`)와 data-model(outbox/correlation_id)도 갱신한다.
## Reference Doc Update Plan
- project-structure.md: `application` ledger port/outbox service, `adapters/ledger`, runtime retry worker, persistence schema 위치를 반영한다.
- runtime-flows.md: fill transition 이후 outbox 적재, retry worker, completed/duplicate/conflict/retryable 흐름을 추가한다.
- interface-contract.md: `LedgerPort.record_trade(...)`, `FillStatus.execution_id/fee`, portfolio-hub ledger endpoint/409 mapping 을 추가한다.
- data-model.md: ledger trade payload fields, outbox work item fields/status/outcome, `correlation_id` 멱등 규칙을 추가한다.
@@ -1,52 +0,0 @@
# BAT-006 Implementation
- Status: complete
- Batch: BAT-006
## Plan Summary
- LedgerPort 계약 + portfolio-hub HTTP 어댑터 + 결정론 stub 원장을 추가하고, 집행 루프 fill 전이 직후에 ledger outbox 에 work item 을 적재하도록 연결했다. `LedgerWriter` 가 outbox 를 drain 해 portfolio-hub write 를 시도하고, NEW/DUPLICATE_OK/conflict/retryable 결과를 `completed/duplicate_completed/retryable_failure/conflict_blocked` outcome 으로 분류한다. paper/KIS 브로커는 결정적 `execution_id` 와 `fee` 를 `FillStatus` 로 운반한다. baseline 4문서를 ledger 레이어로 정합시켰다.
## Changed Files
- 신규
- `trex_core/application/ledger_port.py` — `LedgerPort` Protocol, `LedgerTrade`, `LedgerWriteResult`, `LedgerError`/`LedgerConflictError`/`LedgerRetryableError` (REQ-017).
- `trex_core/application/ledger_outbox.py` — `LedgerOutbox` Protocol, `LedgerWorkItem`, `OutboxOutcome`, `WorkItemStatus`, `LedgerWriter`, `InMemoryLedgerOutbox` (REQ-019).
- `trex_core/adapters/ledger/__init__.py`, `in_memory.py` (`InMemoryLedger` stub), `portfolio_hub.py` (`PortfolioHubLedger` HTTP 어댑터) (REQ-017).
- `trex_core/adapters/persistence/sql/002_create_ledger_outbox.sql` — `ledger_outbox` 테이블, `correlation_id` PK, status/outcome CHECK (REQ-019).
- `tests/adapters/ledger/{__init__,test_in_memory_ledger,test_portfolio_hub_ledger}.py` — REQ-017 AC-1/2/3.
- `tests/adapters/persistence/test_postgresql_ledger_outbox.py` — REQ-019 PG outbox (PG-gated).
- `tests/application/{test_execution_ledger,test_ledger_outbox}.py` — REQ-018 + REQ-019.
- `tests/domain/test_domain_import_sweep.py` — REQ-017 AC-4 (NFR-1) 정적 import 스윕.
- 수정
- `trex_core/application/broker_port.py` — `FillStatus.execution_id/fee/tax` 확장 + 공유 `calculate_fee(side, amount)` 헬퍼 (REQ-018).
- `trex_core/adapters/broker/paper.py` — `filled` 상태에서 `paper:{broker_order_id}:{filled_qty}` execution_id + fee 합성.
- `trex_core/adapters/broker/kis.py` — `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}` execution_id + fee 운반.
- `trex_core/application/execution.py` — `ExecutionService` 가 fill 전이 직후 `LedgerWorkItem` 을 outbox 에 enqueue, `portfolio_id` 부재면 skip, slot 전이 우선 보장 (REQ-018 + REQ-019).
- `trex_core/adapters/persistence/postgresql.py` — `PostgreSQLLedgerOutbox` (`enqueue`/`list_pending`/`get`/`mark_outcome`).
- `trex_core/runtime/config.py` — `PortfolioHubConfig`, `DaemonConfig.portfolio_hub`/`ledger_interval_sec`, env 로딩.
- `trex_core/runtime/daemon.py` — `_build_ledger`, ledger writer 백그라운드 루프, `ExecutionService(..., ledger_outbox=outbox)`.
- `tests/conftest.py`, `tests/adapters/persistence/test_migration_and_drift.py` — `ledger_outbox` 테이블 반영.
- `.env.example` — `TREX_PORTFOLIO_HUB_BASE_URL/TOKEN/TIMEOUT`, `TREX_LEDGER_INTERVAL_SEC` 추가.
## Execution Log
1. baseline 테스트 88 passed / 8 skipped 확인.
2. REQ-017: `LedgerPort` + `LedgerTrade` + 예외 + `InMemoryLedger` stub + `PortfolioHubLedger` 어댑터 추가, contract 테스트 4종 작성.
3. REQ-018: `FillStatus.execution_id/fee/tax` 확장, paper/KIS 결정적 합성 + `calculate_fee` 헬퍼, `ExecutionService._on_filled` 에서 fill 전이 직후 ledger outbox enqueue, `portfolio_id` 부재 시 skip + `ExecutionResult.ledger_enqueued/ledger_skipped` 카운터 추가.
4. REQ-019: `LedgerOutbox` Protocol + `LedgerWorkItem`/`OutboxOutcome`/`WorkItemStatus`, `LedgerWriter` (correlation_id 로컬 dedup + 어댑터 `LedgerWriteResult.DUPLICATE_OK` 매핑 → `duplicate_completed`), `InMemoryLedgerOutbox`, `PostgreSQLLedgerOutbox`, `002_create_ledger_outbox.sql` 마이그레이션.
5. 데몬 lifespan 에 ledger writer thread 추가, `PortfolioHubConfig` env 로딩.
6. REQ-020: runtime-flows / interface-contract / project-structure / data-model 4문서를 ledger 레이어로 정합.
7. 정적 import 스윕 테스트(`tests/domain/test_domain_import_sweep.py`) 로 `trex_core/domain` 의 ledger/HTTP 의존 0 검증.
8. 최종 테스트: 109 passed / 12 skipped (PG·KIS 게이트). ruff lint 0 issues.
## Validation
- 명령: `.venv/bin/python -m pytest -q`
- 결과: `109 passed, 12 skipped, 1 warning`.
- 명령: `.venv/bin/ruff check trex_core tests`
- 결과: `All checks passed!`
- 추가 테스트 21개(REQ-017 7 / REQ-018 3 / REQ-019 7 / NFR-1 import 스윕 1 / PG outbox 4 PG-skip).
- portfolio-hub 어댑터 contract 는 `httpx.MockTransport` 로 path/body/auth/409/5xx/network-error 모두 커버.
- buy/sell lifecycle 테스트로 슬롯 전이와 ledger 기록 분리, dedup, 재시도 결국 기록, 409 무한루프 방지, ledger 장애 시 슬롯 전이 우선 보장을 확인.
## Remaining Risks
- portfolio-hub 의 실제 4xx 응답 본문(특히 `duplicate` 플래그) 계약은 orbit 패턴을 가정한다. 실통합 시 응답 본문이 다르면 `PortfolioHubLedger.record_trade` 의 `LedgerWriteResult.DUPLICATE_OK` 판별을 조정해야 한다. 본 batch 는 stub/contract 수준 evidence 로 검증했다.
- KIS `execution_id` 합성에 사용되는 `ord_dt`/`ord_gno_brno` 가 일부 KIS 응답에서 비어 있으면 결정성이 약화될 수 있다. 현재는 누락 시 `today`/빈 문자열로 폴백한다. 운영 데이터 관찰 후 보강 여부 판단(후속 후보).
- 부분체결(partially_filled) 단계에서는 ledger 기록을 enqueue 하지 않는다. 부분체결 누적 fill 의 정밀 회계는 후속 Discovery 범위로 남는다.
@@ -1,30 +0,0 @@
# BAT-006: portfolio-hub ledger 통합
- Status: released
- Profile: standard
- Source Discovery: dcy-004
- Included REQ: REQ-017, REQ-018, REQ-019, REQ-020
- Excluded REQ: 없음
## Summary
- dcy-004 슬라이스(체결을 외부 회계/포지션 원장에 기록)를 하나의 delivery 단위로 묶는다. BAT-004 의 주문 집행 루프 위에 `LedgerPort` + portfolio-hub 어댑터 + fill hook + ledger outbox 재시도/멱등 처리를 추가하고, baseline 문서를 실제 ledger 레이어와 정합시킨다.
## Scope
- In Scope:
- REQ-017: `LedgerPort.record_trade(...)` 계약, portfolio-hub ledger 어댑터, 결정론 stub/in-memory 원장, `TREX_PORTFOLIO_HUB_*` config.
- REQ-018: 매수/매도 fill 전이 시 ledger 기록 hook, `FillStatus.execution_id/fee` 확장, `asset_id = slot.portfolio_id` 사용, 단일 기록 주체 유지.
- REQ-019: ledger outbox work item, correlation_id 멱등, retry worker, outcome(`completed`/`duplicate_completed`/`retryable_failure`/`conflict_blocked`) 분류.
- REQ-020: runtime-flows/interface-contract/project-structure/data-model ledger baseline 정합.
- Out of Scope (후속 Discovery):
- `post_portfolio_transaction` 입출금 등 거래 외 트랜잭션, asset master 동기화, portfolio-hub ↔ core 양방향 reconciliation, 포지션 재구성, safety floor, 매수·매도 전략 데몬, 실계좌 go-live.
## 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-004 단일. 확정 결정: `asset_id == portfolio_id`, 별도 ledger outbox + retry loop, execution_id 결정적 합성(KIS/paper), fee 는 BrokerPort 계산, tax 기본 0.
- 핵심 리스크: 원장 기록 유실/중복(R-1), portfolio-hub 장애가 execution path 를 막는 결합(R-3). outbox + dedup + adapter 격리 + stub failure injection 으로 완화한다.
- portfolio-hub 실제 계약/자격증명은 orbit `post_ledger_trade` 동일 가정을 시작점으로 두되, verification 에서 adapter contract evidence 또는 미가용 blocker 를 분리 기록한다.
@@ -1,47 +0,0 @@
# BAT-006 Planning
- Status: draft
- Batch: BAT-006
- Profile: standard
## Included REQ
- REQ-017 (Integration): LedgerPort 추상화 + portfolio-hub 어댑터 + stub 원장
- REQ-018 (Integration): 체결→원장 기록 fill hook
- REQ-019 (Exception): ledger outbox durability + correlation_id 멱등
- REQ-020 (Documentation): ledger 레이어 baseline 문서 정합
## Delivery Plan
- 의존 순서대로 구현한다.
1. REQ-017: `trex_core/application` 에 `LedgerPort` 계약과 ledger 예외를 정의하고, `trex_core/adapters/ledger` 에 in-memory/stub 원장과 portfolio-hub HTTP 어댑터를 추가한다.
2. REQ-018: `FillStatus` 가 `execution_id`/`fee` 를 운반하도록 broker port 와 paper/KIS 어댑터를 확장하고, execution fill 전이에 ledger 기록 입력을 연결한다.
3. REQ-019: 직접 write 대신 ledger outbox 적재 + retry worker 로 경로를 고정하고, `correlation_id = {asset_id}:{order_id}:{execution_id}` dedup/outcome 분류를 구현한다.
4. REQ-020: 실제 코드 경계와 data model 이 확정된 뒤 baseline 4문서(runtime-flows/interface-contract/project-structure/data-model)를 갱신한다.
## Dependencies
- Internal:
- REQ-018 은 REQ-017 의 `LedgerPort`와 stub 원장 선행.
- REQ-019 는 REQ-018 의 fill 기록 이벤트를 직접 write 가 아니라 outbox work item 으로 전환.
- REQ-020 은 REQ-017~019 구현 결과에 의존.
- BAT-004 의 execution loop, BrokerPort, FillStatus, PostgreSQL runtime 위에서 확장한다.
- External:
- portfolio-hub ledger API(`POST /v1/assets/{asset_id}/ledger/trades`, orbit `post_ledger_trade` 패턴).
- portfolio-hub URL/token 환경 변수(`TREX_PORTFOLIO_HUB_*`).
- HTTP client, PostgreSQL(outbox persistence), 기존 .env loading(REQ-010).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 application port + external adapter + execution fill lifecycle + outbox durability/retry worker + baseline data model 이 모두 구조/인터페이스/런타임에 영향. 구현 전 `record_trade` 계약, fill event shape, outbox schema/state machine, failure/outcome policy 를 design.md 에 고정해야 한다.
## Milestones
1. M1 — REQ-017: LedgerPort + stub ledger + portfolio-hub adapter contract/409 handling + config.
2. M2 — REQ-018: FillStatus execution_id/fee 확장 + paper/KIS 결정적 값 운반 + buy/sell fill hook.
3. M3 — REQ-019: ledger outbox store + retry worker + correlation_id dedup + conflict/retry outcomes.
4. M4 — REQ-020: baseline 4문서 정합.
5. M5 — 통합 verification: buy/sell lifecycle, duplicate 0, retry eventually completed, conflict_blocked, import sweep.
## Risks
- R-1 (High): 원장 기록 유실/중복. 대응: outbox persistence + correlation_id dedup + duplicate_completed/conflict_blocked outcome + failure injection tests.
- R-2 (High): fill 전이와 ledger write 결합으로 슬롯 전이 지연/실패. 대응: 슬롯 전이는 우선 진행하고 ledger write 는 outbox retry 로 분리.
- R-3 (Medium): portfolio-hub 계약/자격증명 미가용. 대응: stub/in-memory 완전 검증, adapter contract test, 실제 통합은 evidence 또는 blocker 로 분리.
- R-4 (Medium): 도메인 순수성/단일 기록 주체 침식. 대응: LedgerPort 는 application 계약, HTTP 는 adapters 격리, `trex_core/domain` import sweep, 기록 주체는 execution path 로 한정.
- R-5 (Medium): REQ-020 baseline 이 구현보다 먼저 확정되어 drift 발생. 대응: baseline 갱신은 M4 로 후행하고 verification 에 코드-문서 정합 점검 포함.
@@ -1,79 +0,0 @@
# BAT-006 Verification
- Status: approved
- Batch: BAT-006
## Acceptance Mapping
- REQ-017:
- AC-1: in-memory stub 원장 `record_trade(...)` 1회 호출 시 거래 1건 적재와 필드 보존 evidence.
- AC-2: portfolio-hub adapter 가 `POST /v1/portfolios/{portfolio_id}/ledger/trades` 에 `execution_id` 포함 payload(body `quantity`/`fee_amount`/`tax_amount`)를 전송하는 contract evidence. (정정: REQ-017 CHG-20260606-01 — 아래 Re-verification Addendum)
- AC-3: portfolio-hub 409 응답이 `LedgerConflictError` 또는 `conflict_blocked` 경로로 표면화되는 evidence.
- AC-4: `trex_core/domain` portfolio-hub/HTTP 의존 0 import sweep evidence.
- REQ-018:
- AC-1: idle 슬롯 매수 intent 체결 시 holding 전이 + buy ledger item/record 1건 evidence.
- AC-2: holding 슬롯 매도 intent 체결 시 closed 전이 + sell ledger item/record 1건 evidence.
- AC-3: `FillStatus` 가 결정적 `execution_id`, fee, tax=0 을 운반하는 evidence.
- AC-4: `portfolio_id` 부재 체결 정책(skip/block)과 슬롯 전이 일관성 evidence.
- AC-5: ledger 기록 주체가 execution path 로 한정되는 code review/import evidence.
- REQ-019:
- AC-1: 동일 execution_id 2회 처리 시 원장 중복 0 evidence.
- AC-2: retryable portfolio-hub failure 후 retry 로 eventually completed evidence.
- AC-3: 409 conflict 가 `conflict_blocked` 로 남고 무한 자동 재시도하지 않는 evidence.
- AC-4: 이미 기록된 work item 재처리 시 `duplicate_completed` evidence.
- AC-5: portfolio-hub 불가 직후에도 슬롯 전이는 진행되는 분리 evidence.
- REQ-020:
- AC-1: 4개 baseline 문서에 ledger 레이어 반영 evidence.
- AC-2: LedgerPort, FillStatus, outbox 구조, 원장 필드 설명이 코드와 모순되지 않는 review evidence.
- AC-3: baseline 문서 변경 이력에 REQ-017~020 반영 기록 evidence.
## Evidence
### Test
- 명령: `.venv/bin/python -m pytest -q`
- 결과: `109 passed, 12 skipped, 1 warning in 0.98s` (PG 4건 + KIS 1건 + 기존 PG 7건은 DSN/credentials 게이트로 skip).
- REQ-017 AC-1: `tests/adapters/ledger/test_in_memory_ledger.py::test_record_trade_preserves_fields_and_is_idempotent_on_execution_id` — 단일 호출 후 `count()==1` + 필드 보존, 동일 execution_id 재호출 후에도 `count()==1`.
- REQ-017 AC-2: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_post_contract_includes_execution_id_and_path_uses_portfolio_id` — `httpx.MockTransport` 로 path `/v1/portfolios/PF-77/ledger/trades`, body 에 `execution_id`/`side`/`quantity`/`fee_amount`/`tax_amount` 포함(+`qty`/`fee`/`tax` 부재), `Authorization: Bearer …` 헤더 캡처. (정정: REQ-017 CHG-20260606-01 — 아래 Re-verification Addendum)
- REQ-017 AC-3: `tests/adapters/ledger/test_portfolio_hub_ledger.py::test_409_surfaces_as_ledger_conflict_error` + `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` — 409 → `LedgerConflictError`; outbox row `status=conflict_blocked`, 후속 writer tick 에서 어댑터 재호출 없음.
- REQ-017 AC-4: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` — `trex_core/domain` AST 워크가 `httpx`/`fastapi`/`uvicorn`/`psycopg`/`redis`/`trex_core.adapters`/`trex_core.runtime`/`trex_core.application` 0 회 import 확인.
- REQ-018 AC-1: `tests/application/test_execution_ledger.py::test_buy_fill_enqueues_one_ledger_item_and_writer_records_it` — buy intent → execution.run_once → slot=holding, ledger_enqueued=1, writer.run_once → ledger.count()=1, `LedgerTrade(asset_id="PF-1", side="buy", qty=10, price=1000.0)`.
- REQ-018 AC-2: `tests/application/test_execution_ledger.py::test_sell_fill_enqueues_sell_ledger_item` — buy→holding→sell intent→execution.run_once → slot=closed, ledger 에 `["buy","sell"]` 순서.
- REQ-018 AC-3: 위 buy 테스트가 `t.execution_id.startswith("paper:PAPER-")`, `t.fee > 0.0`, `t.tax == 0.0` 직접 검증.
- REQ-018 AC-4: `tests/application/test_execution_ledger.py::test_portfolio_id_absent_skips_ledger_but_transitions_slot` — portfolio_id=None → ledger_enqueued=0, ledger_skipped=1, 슬롯은 HOLDING.
- REQ-018 AC-5: `tests/domain/test_domain_import_sweep.py` (도메인 격리) + `tests/application/test_execution_ledger.py` (오직 `ExecutionService._record_ledger` 만 outbox.enqueue 호출 — code review). `grep -r "ledger_outbox.enqueue\|LedgerOutbox.enqueue" trex_core/` 결과는 `ExecutionService` 단일 호출지점.
- REQ-019 AC-1: `tests/application/test_execution_ledger.py::test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 같은 trade 재 enqueue 시 outbox row 1개 유지, 별도 outbox 에서 재처리해도 ledger.count()=1.
- REQ-019 AC-2: `tests/application/test_execution_ledger.py::test_retryable_failure_then_eventually_completes` + `tests/application/test_ledger_outbox.py::test_writer_retries_until_success` — retryable 2회 + completed 1회로 ledger.count()=1, 슬롯은 즉시 HOLDING.
- REQ-019 AC-3: `tests/application/test_execution_ledger.py::test_conflict_blocks_without_auto_retry` + `tests/application/test_ledger_outbox.py::test_writer_marks_conflict_blocked_and_does_not_retry` — 후속 writer tick stats 모두 0.
- REQ-019 AC-4: `tests/application/test_execution_ledger.py::test_duplicate_completed_when_reprocessing_terminal_item` + `test_duplicate_execution_id_results_in_zero_duplicate_in_ledger` — 별도 outbox 에 동일 trade 재 enqueue → writer.run_once stats.duplicates=1, stats.completed=0, ledger.count()=1.
- REQ-019 AC-5: `tests/application/test_execution_ledger.py::test_slot_transition_proceeds_when_ledger_is_down` — `InMemoryLedger(fail_n_then_succeed=100)` 으로 모든 writer tick 실패, slot 은 HOLDING, outbox row `status=PENDING` 유지(무유실).
- PG 통합 (DSN 제공 시): `tests/adapters/persistence/test_postgresql_ledger_outbox.py` 4건 — enqueue 멱등, writer completed/conflict_blocked/retryable 행 컬럼 검증. CI 환경에 DSN 없어 skip 4건은 명시적으로 PG 게이트.
### Manual
- baseline 문서 ↔ 코드 정합 리뷰 (REQ-020 AC-1/AC-2):
- `docs/runtime-flows.md` 새 섹션 `Flow: 체결 → ledger outbox` + `Flow: ledger writer` 가 `ExecutionService._record_ledger` (`trex_core/application/execution.py:117-141`) 과 `LedgerWriter.run_once`/`_process` (`trex_core/application/ledger_outbox.py:96-149`) 로직 정합. paper/KIS execution_id 합성 규칙은 `trex_core/adapters/broker/paper.py:73-79`, `trex_core/adapters/broker/kis.py:177-192` 와 일치.
- `docs/interface-contract.md` 새 섹션 `Interface: LedgerPort` + `Interface: LedgerOutbox/LedgerWriter` 가 `trex_core/application/ledger_port.py` 시그니처, `LedgerWriteResult.NEW/DUPLICATE_OK`, `LedgerConflictError`/`LedgerRetryableError` 와 정합. portfolio-hub endpoint 표는 `trex_core/adapters/ledger/portfolio_hub.py:_payload`/`record_trade` 와 정합.
- `docs/project-structure.md` 디렉토리 트리에 `trex_core/application/ledger_port.py`, `ledger_outbox.py`, `trex_core/adapters/ledger/`, persistence 의 `PostgreSQLLedgerOutbox` + `002_create_ledger_outbox.sql`, runtime 의 ledger writer 루프가 반영. 실제 디렉토리 트리(`find trex_core -type f -name '*.py'`)와 일치 확인.
- `docs/data-model.md` 새 엔티티 `LedgerTrade`(application DTO) + `ledger_outbox` 테이블 정의가 `002_create_ledger_outbox.sql` (PK `correlation_id`, status/last_outcome CHECK) 와 1:1 정합. `FillStatus` 확장(`execution_id/fee/tax`) 도 명시.
- code-search (REQ-020 AC-2 + REQ-018 AC-5 보조):
- `grep -r "LedgerOutbox.enqueue\|self._ledger_outbox.enqueue" trex_core/` → `trex_core/application/execution.py` 단 1곳(execution path 단일 기록 주체).
- `grep -r "import httpx\|from httpx" trex_core/domain/` → 결과 0(NFR-1 보강).
### Logs
- `.venv/bin/ruff check trex_core tests` → `All checks passed!` (style/lint blocker 0).
- 정적 import 스윕 자동화: `tests/domain/test_domain_import_sweep.py` 가 CI 의 일부로 회귀 가드.
- portfolio-hub 실 통합 evidence 는 본 batch 범위 밖(`PortfolioHubLedger` 는 `httpx.MockTransport` 로 contract 검증). 실 통합은 운영 환경에서 수집 — `Remaining Risks` 와 `docs/srs/Integration/req-017_…md` Notes 의 A-1/DATA 위임 항목으로 추적.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음.
- Notes:
- portfolio-hub 실 계약/자격증명 evidence 는 stub + contract 수준이며, 실 통합 검증은 운영 단계에서 수집한다(dcy-004 DATA 위임 사항).
- 부분체결 누적/정밀 세금 모델은 후속 Discovery 로 분리됨(이번 범위 밖).
## Re-verification Addendum (2026-06-06, REQ-017 CHG-20260606-01)
- 트리거: RabbitMQ 매수 인텐트 → 체결 → ledger 기록 실 e2e 중 ledger write 가 live portfolio-hub 에서 404 발생. 원인은 어댑터가 구 orbit 가정 계약(`POST /v1/assets/{asset_id}/ledger/trades`, body `qty`/`fee`/`tax`)을 사용한 것. 실 계약은 openapi `LedgerTradeRequest` 기준 `POST /v1/portfolios/{portfolio_id}/ledger/trades` + `quantity`/`fee_amount`/`tax_amount`(성공 201).
- 정정 구현: `trex_core/adapters/ledger/portfolio_hub.py` (path + body 키), `tests/adapters/ledger/test_portfolio_hub_ledger.py` (실 계약 단언).
- 재검증 결과:
- 단위/통합: `.venv/bin/python -m pytest tests/adapters/ledger/ tests/application/test_execution_ledger.py -q` → `16 passed`. `.venv/bin/ruff check trex_core/adapters/ledger tests/adapters/ledger` → All checks passed.
- 실 e2e (live portfolio-hub 127.0.0.1:18000): 데몬 기동 → 매수 인텐트(005930, 10주, 300,000) 어드미션 → 슬롯 `idle→pending_buy→holding`(qty=10, avg_price=300000) → ledger writer `POST /v1/portfolios/{portfolio_id}/ledger/trades` **201 Created** → `ledger_outbox.status=completed`(last_outcome=completed). 직전 404 로 쌓였던 outbox 항목이 정정 후 재기동 시 무유실 드레인되어 REQ-019 durability 도 동시 실증(attempts 누적, 유실 0).
- 영향: REQ-017 Implemented 유지(정정 후 실 계약 일치). REQ-018/019/020 evidence 는 영향 없음(포트/실행 경로/outbox 시맨틱 불변). REL-001(confirmed) 의 ledger write release blocker 해소.
@@ -1,56 +0,0 @@
# BAT-007 Design
- Status: draft
- Batch: BAT-007
## Architecture Summary
- 기존 FastAPI/uvicorn HTTP `/v1` 어댑터를 제거하고, trex-core ↔ 전략 모듈 통신을 **RabbitMQ(AMQP) 기반 메시지 transport** 로 전환한다. **application** 에 `MessagingTransport` Protocol 을 두고, **adapters/messaging** 에 `RabbitMQTransport`(`pika` 동기) 와 `InMemoryTransport`(stub) 구현체를, **runtime** 에 sync+thread daemon(consumer/execution/ledger 스레드 + 이벤트 publisher) 구성을 둔다. 토폴로지는 3 패턴(스냅샷 RPC + intent admission RPC + 이벤트 topic) 이며, intent 는 슬롯 Postgres durable commit 후 ack + correlation_id dedup 으로 effectively-once 효과를 낸다. broker 장애 시 어댑터의 connection 상태가 application 레이어로 노출되어 신규 매매를 보수적으로 중단한다(버퍼링 0). HTTP 표면은 `healthz` 포함 0 으로 떨어지고 liveness 는 프로세스 감독이 담당한다.
## Changed Areas
- Docs: interface-contract(메시지 계약 + 어댑터 신선도 노출, HTTP `/v1` 제거), runtime-flows(스냅샷 RPC / intent RPC / 이벤트 발행·구독 / degrade 분기 / 스레드 구성 / HTTP 흐름 제거), project-structure(adapters/messaging 신설, adapters/api 제거, runtime/daemon 의 스레드 구성과 의존성 변경).
- Modules:
- application: 신규 `messaging.py`(`MessagingTransport` Protocol + RPC 요청/응답 DTO + 이벤트 DTO). 기존 `intents.py`/(dcy-005 스냅샷 서비스 자리) 는 transport-무관 호출 인자만 받는다.
- adapters/messaging: 신규 패키지. `topology.py`(exchange/queue/binding/routing-key 상수 + 매핑 헬퍼), `in_memory.py`(결정론 stub), `rabbitmq.py`(pika BlockingConnection 기반 publish/subscribe/RPC, 스레드별 channel, 재연결 백오프).
- adapters/api: **제거** (`app.py`, `schemas.py`, `__init__.py`).
- runtime: `config.py` 에 `RabbitMQConfig(url, timeout)` 추가, `TREX_RABBITMQ_URL` 로딩, `TREX_API_HOST`/`TREX_API_PORT` 폐기. `daemon.py` 에서 uvicorn import/실행 제거, FastAPI base 제거, intent RPC consumer 스레드 + snapshot RPC consumer 스레드 추가, 기존 execution/ledger 스레드 유지, graceful shutdown 에 connection close 포함.
- persistence: 변경 없음 (intent_log 를 dedup 게이트로 재사용; 단, RabbitMQ consumer 전용 dedup 컬럼이 필요하면 design 결정 1 참조).
- pyproject: `pika>=1.3` 추가, `fastapi`/`uvicorn` 제거 (httpx 는 ledger 어댑터가 유지).
- Interfaces: `MessagingTransport.publish_event(routing_key, payload, mandatory=True)`, `MessagingTransport.consume_rpc(queue, handler)`, `MessagingTransport.call_rpc(queue, payload, timeout)`, 그리고 토폴로지 상수 + routing-key 빌더.
## Key Decisions
1. **dedup 게이트 = intent_log 재사용(dcy-003/REQ-007 멱등 게이트 직접 활용)**. RabbitMQ intent consumer 는 `correlation_id`(= intent 멱등키, dcy-003 패턴)를 `IntentStore.try_insert`(application port) 의 `intent_id` 로 그대로 사용해 단일 dedup 진실원을 유지한다. consumer 전용 dedup table 은 두지 않는다 — at-least-once 재전달 시 두 번째 호출은 `try_insert==False` 로 끝나 슬롯 전이 0, 첫 결과를 그대로 response 로 회신한다. (장점: 새 스키마 0, 기존 멱등 가드 직접 활용. 단점: intent admission RPC 외 다른 RPC 가 늘면 별도 dedup 전략 필요 — 본 batch 범위에서는 intent + read-only snapshot 만 있어 충분.)
2. **ack-after-commit 시퀀스**: intent consumer 는 (a) `IntentService.submit_buy/sell` 호출 → (b) Postgres durable commit 안에서 `intent_log` row 와 슬롯 전이가 함께 atomically commit → (c) reply-to 큐로 admission 응답 publish → (d) 메시지 manual ack. (b)와 (c) 사이 크래시는 재전달 시 (1) 의 dedup 으로 재실행되지 않고, (c)와 (d) 사이 크래시는 재전달 시 같은 응답을 다시 보내고 ack — 동일 `correlation_id` 응답은 호출자가 dedup. publisher confirms 로 reply publish 의 broker 수락을 확인.
3. **단일 consumer + prefetch=1**: intent RPC consumer 는 단일 스레드 + `basic_qos(prefetch_count=1)` 로 FIFO 보장(불변식 A 단일 writer 와 정합). snapshot RPC consumer 는 read-only 이므로 prefetch>1 허용 가능하지만 본 batch 는 단일 + prefetch=1 로 일관성 우선.
4. **이벤트 페이로드 = 현재 상태 + 단조 시퀀스(슬롯 `updated_at` 또는 별도 version 컬럼)**. delta 가 아니라 last-wins 가능한 스냅샷을 운반해 순서 보증 불요. 미수신/중복은 스냅샷 RPC reconcile 로 보정. `daily_slot.updated_at` 을 시퀀스로 사용(이미 존재, 새 컬럼 0).
5. **publisher confirms + DLQ**: 이벤트 publisher 는 `confirm_delivery()` 모드 + `basic_publish(mandatory=True)`. 발행 거부(NACK) 또는 unroutable 은 별도 `trex.events.dlq` 큐로 라우팅, last_error 로그. intent RPC reply publish 도 동일.
6. **per-strategy durable queue + TTL/max-length**: 전략별 이벤트 queue 는 durable, queue 인자 `x-message-ttl=3600000`(1h) + `x-max-length=10000` + `x-overflow=drop-head`(drop-oldest). 초과/만료는 스냅샷 RPC reconcile 로 회복.
7. **재연결 백오프 상태 머신**: `connected → disconnected(트리거: socket error/heartbeat timeout) → reconnecting(백오프 1s,2s,4s,8s,...,30s cap) → connected`. 재연결 성공 후 토폴로지 재선언 + consumer rebind 책임은 어댑터 내부. application 레이어는 `transport.health()` 로 enum 조회.
8. **스레드별 channel + add_callback_threadsafe**: pika `BlockingConnection` 은 thread-safe 가 아니므로 (a) 각 consumer 스레드는 자기 connection+channel 을 소유하고 (b) execution/ledger 스레드의 이벤트 발행은 publisher 스레드 channel 의 `add_callback_threadsafe` 로 enqueue 한다. 또는 별도 publisher worker 스레드 도입 — design 검토 후 단순화를 위해 publisher worker 스레드 1개 + queue 기반 cross-thread enqueue 채택.
9. **HTTP 표면 0 회귀 가드**: 데몬 lifecycle 테스트가 기동 후 `import trex_core.runtime.daemon` 결과에 `fastapi`/`uvicorn`/`starlette` import 가 없음을 정적 가드로 검사. 추가로 포트 스캔(socket bind 0 검증) 통합 테스트(미가용 시 skip).
10. **broker outage = transport.health() != connected 일 때 application 분기**. `IntentService` 진입점은 변경하지 않고, 새 `intent RPC consumer` 가 message 를 수신해도 transport.health() == disconnected 면 reply publish 가 어차피 실패 → 그 경우 메시지 reject(requeue=False) + DLQ 로 격리(전략은 timeout 으로 감지 후 보류). 스냅샷 RPC 도 동일. **버퍼링 금지**: 어댑터에 로컬 outbox 를 두지 않는다(ledger outbox 는 별도 — broker→ledger 가 아니라 core→portfolio-hub 방향이라 영향 없음).
## Edge Cases
- 동일 `correlation_id` 의 intent 메시지가 재전달 → `intent_log.try_insert==False` → 기존 결과 fetch 후 reply, 슬롯 전이 0.
- intent commit 직후 reply publish 전 크래시 → 재기동 후 메시지 재전달 → dedup 으로 같은 reply 재전송 → 호출자 dedup. 응답은 같은 `correlation_id`.
- pika connection drop 도중 publish/consume → 자동 재연결 백오프 + 채널 재선언 + consumer rebind. 미전송 message 는 broker 가 redeliver 시 dedup.
- 전략 queue down 중 trex-core 이벤트 발행 → durable queue 가 catch-up 보관 → 전략 복구 시 수신. TTL/max-length 초과분은 drop-oldest + reconcile.
- portfolio-hub 가 아닌 RabbitMQ 가 down → 신규 intent 거부(requeue=False + DLQ) + 스냅샷 RPC timeout → 전략 보류. core 자율 안전 매도(미구현, A-5)는 transport 와 독립 동작 전제.
- 전략 모듈 미전환 상태에서 cut-over 시 통신 단절 → release 단계 cut-over 순서(전략 모듈 → trex-core release) 합의 + stub 사전 검증으로 회피(R-2).
- HTTP 클라이언트(이전 전략 모듈/외부 tool)가 옛 `POST /v1/intents/buy` 호출 시도 → connection refused (HTTP 표면 0). release notes 에 cut-over 경고 명시.
## Architecture Impact
- both (project-structure + runtime-flows). interface-contract 도 갱신한다(메시지 계약 + HTTP `/v1` 제거).
## Reference Doc Update Plan
- project-structure.md:
- 트리에서 `trex_core/adapters/api/` 제거, `trex_core/adapters/messaging/` (`topology.py`, `rabbitmq.py`, `in_memory.py`) 추가.
- `trex_core/application/messaging.py` (Protocol + DTO) 추가.
- runtime/daemon 의 스레드 구성(intent RPC + snapshot RPC + execution + ledger + publisher worker) 반영.
- 의존성 변경(`pika` 추가, `fastapi`/`uvicorn` 제거) 반영.
- runtime-flows.md:
- 기존 매수/매도 intent HTTP 흐름 제거.
- 신규: 스냅샷 RPC 흐름, intent admission RPC 흐름(2단계 시맨틱: admission 동기 + fill 비동기 이벤트), 체결/슬롯 이벤트 발행·구독 흐름(routing key `{event_type}.{strategy_id}.{symbol}` + per-strategy queue), broker degrade 분기 흐름, 재연결 백오프, 스레드 구성.
- interface-contract.md:
- HTTP `/v1` 섹션 제거.
- 신규: 메시지 계약(스냅샷 RPC `trex.snapshot.req` / intent RPC `trex.intent.req` / 이벤트 topic `trex.events`), routing key 형식, RPC payload/response 셰이프, `correlation_id`/reply-to 규약, durable/persistent/ack 정책, publisher confirms + DLQ, TTL/max-length, 어댑터 신선도 상태(`connected/disconnected/reconnecting`) 노출.
- data-model.md: 변경 없음 — 이벤트 페이로드는 entity 가 아니라 interface contract 가 담는 편이 자연스러움(REQ-026 Notes 참조).
@@ -1,66 +0,0 @@
# BAT-007 Implementation
- Status: complete
- Batch: BAT-007
## Plan Summary
- `MessagingTransport` Protocol(application) + `InMemoryTransport` stub + `RabbitMQTransport`(pika 동기) 어댑터를 신설하고, 데몬을 sync+thread 4 스레드(intent RPC consumer + snapshot RPC consumer + execution + ledger) 구조로 재구성했다. intent RPC consumer 는 `IntentService.submit_*` 를 호출해 Postgres durable commit 후 manual ack 하며, `intent_log` 멱등 게이트가 중복 메시지를 흡수한다. ExecutionService 에 event emitter hook 을 더해 fill 전이 직후 `trex.events` 토픽으로 routing key `{event_type}.{strategy_id}.{symbol}` 이벤트를 발행한다(publisher confirms + DLQ). 어댑터의 connection 상태가 application 으로 노출되어 broker outage 시 신규 매매가 보수적으로 중단된다. `adapters/api/` 트리와 `fastapi`/`uvicorn` 의존을 제거하고 baseline 3문서를 정합시켰다.
## Changed Files
- 신규
- `trex_core/application/messaging.py` — `MessagingTransport` Protocol, `RpcRequest`/`RpcResponse`, `TransportHealth`/`TransportUnavailableError`/`RpcTimeoutError`.
- `trex_core/application/messaging_handlers.py` — `make_intent_rpc_handler` / `make_snapshot_rpc_handler` / `slot_transition_event_payload`.
- `trex_core/adapters/messaging/__init__.py`, `topology.py`(상수 + routing key 빌더 + STRATEGY_QUEUE_ARGS), `in_memory.py`(결정론 stub + 가용성 토글 + DLQ 수집), `rabbitmq.py`(pika BlockingConnection + 재연결 백오프 + publisher confirms + manual ack-after-handler).
- `tests/adapters/messaging/{__init__,test_topology,test_in_memory_transport,test_rabbitmq_transport,test_rabbitmq_intent_e2e}.py` — REQ-021/022/023 검증 (RabbitMQ 통합 5종 + intent dedup E2E 2종 + stub/토폴로지 11종).
- `tests/application/{test_messaging_intent_consumer,test_messaging_snapshot_consumer,test_messaging_event_publisher}.py` — REQ-021/022/023/024 handler·이벤트 흐름·degrade 분기.
- `tests/runtime/test_daemon_no_http_surface.py` — REQ-025 HTTP 표면 0 회귀 가드 5종.
- 수정
- `trex_core/application/execution.py` — `SlotEventEmitter` callback, `ExecutionService.__init__` 에 `event_emitter` 인자 추가, `_on_filled` 에서 transition + ledger enqueue 후 `_emit_event` 호출(publish 실패가 슬롯 전이를 롤백하지 않음).
- `trex_core/runtime/config.py` — `RabbitMQConfig` 추가, `TREX_RABBITMQ_URL`/`TREX_RABBITMQ_TIMEOUT` 로딩, `DaemonConfig.host/port` 와 `TREX_API_HOST`/`TREX_API_PORT` 폐기.
- `trex_core/runtime/daemon.py` — uvicorn/FastAPI import 제거, `Daemon` 클래스로 4 스레드(execution/ledger/intent-rpc/snapshot-rpc) + 이벤트 publisher transport + `signal` 기반 graceful shutdown + connection close 보장.
- `tests/domain/test_domain_import_sweep.py` — 금지 prefix 에 `pika`, `starlette` 추가.
- `tests/conftest.py` — 게이트 env 설명을 RabbitMQ/KIS 포함하도록 갱신.
- `pyproject.toml` — `pika>=1.3` 추가, `fastapi`/`uvicorn` 제거, `httpx` 를 dev → runtime 의존성으로 이동(ledger 어댑터가 사용).
- `.env.example` — `TREX_RABBITMQ_URL`/`TREX_RABBITMQ_TIMEOUT` 추가, `TREX_API_HOST`/`TREX_API_PORT` 제거.
- `docs/runtime-flows.md` / `docs/interface-contract.md` / `docs/project-structure.md` — messaging 레이어 반영, HTTP `/v1` 섹션 제거, sync+thread 데몬 + 4 스레드 구성 반영 (REQ-026).
- `docs/discovery/dcy-002_…md` — transport supersede 메모 (REQ-025 AC-5 cross-reference).
- 제거
- `trex_core/adapters/api/` 전체 (`app.py`, `schemas.py`, `__init__.py`).
- `tests/adapters/api/` 전체.
## Execution Log
1. baseline 테스트 109 passed / 12 skipped 확인.
2. RabbitMQ 가용성 확인: `127.0.0.1:5672` (admin/admin) 으로 `pika.BlockingConnection` 성공.
3. `pika>=1.3` 설치, `pyproject.toml` 의 `fastapi`/`uvicorn` 제거.
4. REQ-021: `topology.py` 에 토폴로지 상수 + routing key 빌더 + STRATEGY_QUEUE_ARGS(TTL/max-length/drop-head/DLX) 추가.
5. REQ-022: `application/messaging.py` (Protocol + DTO + TransportHealth), `adapters/messaging/in_memory.py` (process-local pub/sub + sync RPC + 가용성 토글), `adapters/messaging/rabbitmq.py` (pika BlockingConnection + 재연결 백오프 + publisher confirms + manual ack-after-handler).
6. REQ-021/022/023: `application/messaging_handlers.py` 에 intent/snapshot RPC handler + 이벤트 payload builder. intent handler 는 `correlation_id` 를 intent_id 로 재사용해 `intent_log` 멱등 게이트(REQ-007)를 dedup source 로 활용.
7. REQ-023: ExecutionService 에 event emitter hook 추가. publisher confirms + DLQ 라우팅은 RabbitMQTransport 가 담당. per-strategy durable queue 는 stub 과 실 broker 둘 다 검증.
8. REQ-024: `TransportHealth` enum, `TransportUnavailableError` 즉시-실패, 재연결 백오프(1s..30s cap), stub `set_available(False)` 회귀 가드.
9. REQ-025: `adapters/api/` 전체 제거, `runtime/daemon.py` 를 `Daemon` 클래스 + 4 스레드 + signal handler 로 재작성, `runtime/config.py` 의 `host`/`port` 필드 + `TREX_API_HOST/PORT` 로딩 제거. `.env.example` 정리.
10. REQ-026: baseline 3문서(runtime-flows / interface-contract / project-structure) messaging 섹션 신설 + HTTP `/v1` 섹션 제거 + sync+thread 4 스레드 구성 반영. dcy-002 헤더에 transport supersede 메모 추가(dcy-005 는 이미 cross-reference 완료).
11. 회귀 가드 추가: `tests/runtime/test_daemon_no_http_surface.py` (import 가드 + `adapters/api` 트리 0 + grep fastapi/uvicorn 0 + pyproject 0 + config env 0), `tests/domain/test_domain_import_sweep.py` 에 pika/starlette 금지 prefix.
12. 최종 검증: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/"` 로 `pytest -q` → **135 passed / 12 skipped** (PG/KIS 게이트만 skip; 실 RabbitMQ 통합 7건 모두 실행/통과). `ruff check trex_core tests` → All checks passed.
## Validation
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" .venv/bin/python -m pytest -q`
- 결과: `135 passed, 12 skipped`.
- 명령: `.venv/bin/ruff check trex_core tests`
- 결과: `All checks passed!`.
- 추가 테스트 ~26개:
- 토폴로지: 4종.
- InMemoryTransport stub: 5종(routing pattern, RPC dispatch, degrade, DLQ).
- RabbitMQTransport 통합: 5종(topology declare 멱등, RPC roundtrip, per-strategy event filter, RPC timeout, close).
- RabbitMQ intent E2E: 2종(correlation_id dedup, queue topology PRECONDITION assertion).
- intent handler: 7종(admit, dedup, no_idle, sell from holding, bad payload, unknown type, correlation_id fallback).
- snapshot handler: 3종(all/open view, strategy filter).
- event publisher: 3종(routing + state + sequence, per-strategy filter, publish failure non-blocking).
- HTTP-표면-0 가드: 5종(import 가드 + 트리 부재 + grep + pyproject + config env).
- RabbitMQ 통합 evidence: 실 broker(127.0.0.1:5672, admin/admin) 로 topology declare, RPC roundtrip, per-strategy queue 필터, RPC timeout, intent E2E dedup 통과.
## Remaining Risks
- core 자율 안전 매도(A-5, REQ-024 Notes) 는 본 batch 미구현. broker 장기 outage 시 열린 포지션 자동 보호 없음 — 운영자 수동 대응 의존. 별도 Discovery 로 트래킹 필요.
- 전략 모듈 측 RabbitMQ 클라이언트 전환은 본 batch 범위 외. release 단계에서 cut-over 순서(전략 모듈 → trex-core) 합의 필요 (R-2 / R-5 dcy-006 risk).
- pika thread-safety 우회는 스레드별 transport 인스턴스 + 짧은 채널 lifecycle 로 단순화했으나, 장기 부하/연결 누수 가능성은 운영 환경 모니터링으로 보강 필요(본 batch 단위 테스트로는 충분).
- snapshot RPC 응답 셰이프는 dcy-005 logical 계약이 아직 REQ 정규화되지 않아 stub-shape 수준(슬롯 목록 + capacity). dcy-005 REQ 확정 시 후속 batch 에서 정합 보강(R-7).
- `httpx` 의존을 dev → runtime 으로 이동했다. `portfolio-hub` ledger 어댑터(BAT-006) 가 실제로 사용하므로 적정 위치이며 BAT-006 verification 에 영향 없음.
@@ -1,32 +0,0 @@
# BAT-007: 전략 모듈 RabbitMQ transport 전환
- Status: released
- Profile: standard
- Source Discovery: dcy-006
- Included REQ: REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026
- Excluded REQ: 없음
## Summary
- dcy-006 transport 슬라이스(trex-core ↔ 전략 모듈 통신을 HTTP `/v1` API 에서 RabbitMQ(AMQP) 로 완전 전환)를 하나의 delivery 단위로 묶는다. 토폴로지/계약(REQ-021) → transport 포트 + RabbitMQ 어댑터(`pika` 동기) + stub + sync+thread daemon 재구성(REQ-022) → delivery 신뢰성(effectively-once, durable, DLQ, publisher confirms, REQ-023) → 가용성 degrade 정책(NFR-3, REQ-024) → HTTP `/v1` API + healthz + adapters/api + fastapi/uvicorn 의존 완전 제거(REQ-025) → baseline 3문서 정합(REQ-026) 까지 한 batch 로 deliver 한다.
## Scope
- In Scope:
- REQ-021: 통신 토폴로지(스냅샷 RPC `trex.snapshot.req`, intent admission RPC `trex.intent.req` + RabbitMQ direct reply-to + `correlation_id`, 이벤트 topic exchange `trex.events` + routing key `{event_type}.{strategy_id}.{symbol}`) 와 요청/이벤트 매핑 원칙(view·strategy_id payload, event routing key) 선언.
- REQ-022: `trex_core/application/messaging.py` 신규 `MessagingTransport` Protocol, `trex_core/adapters/messaging/{rabbitmq,in_memory}.py`, 데몬을 sync+thread (intent RPC consumer + snapshot RPC consumer + execution/ledger 스레드)로 재구성, pika 자동 재연결 직접 구현. NFR-1 (도메인 순수성) import 스윕 가드 확장.
- REQ-023: intent durable+persistent + manual ack-after-commit + dedup(dcy-003 correlation_id), 단일 consumer + prefetch=1 FIFO, 이벤트 durable per-strategy queue + 상태/단조 시퀀스 운반 + last-wins, publisher confirms + DLQ, TTL/max-length drop-oldest + 스냅샷 reconcile.
- REQ-024: broker outage 시 버퍼링 금지 + 신규 매매 보수적 중단, 어댑터 connection 신선도(`connected/disconnected/reconnecting`) 노출, core 자율 안전 매도 독립 정책 contract 명시(구현은 별도 Discovery, A-5).
- REQ-025: `trex_core/adapters/api/` 전체 제거, `trex_core/runtime/daemon.py` 에서 uvicorn import/실행 제거, `runtime/config.py` 의 `TREX_API_HOST`/`TREX_API_PORT` 폐기, `pyproject.toml` 의 `fastapi`/`uvicorn` 의존 정리, dcy-002·dcy-005 의 HTTP 가정 cross-reference 갱신.
- REQ-026: `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 정합 (메시지 계약·MQ 흐름·messaging 어댑터·sync+thread 런타임 반영, HTTP `/v1` 섹션 제거).
- Out of Scope (후속 Discovery / 다른 Discovery 소관):
- 스냅샷/intent 의 logical 계약 자체 변경(dcy-005/002/003 소관), 전략 의사결정 로직(전략 모듈 책임), RabbitMQ 클러스터/HA 운영 세부, portfolio-hub 등 다른 외부 연동의 transport, core 자율 안전 매도(stop-loss·forced_close 자동 트리거) 실제 구현(A-5, 별도 Discovery).
## 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-006 단일. 핵심 확정 결정: RabbitMQ 가 HTTP 를 **대체**(병행 없음, OQ-1) + HTTP 표면 0(`healthz` 포함, OQ-7) + 3패턴 토폴로지(스냅샷 RPC + intent RPC + 이벤트 topic, OQ-2) + effectively-once(at-least-once + idempotency, OQ-3) + 보수적 degrade(버퍼링 금지·신규 매매 중단·core 안전 매도 독립, OQ-4) + pika 동기 + sync+thread 런타임(OQ-5) + `TREX_RABBITMQ_URL` env(OQ-6).
- 핵심 리스크: 가용성 결합 — RabbitMQ 가 SPOF 성격(R-3 High, HTTP fallback 없음). REQ-024 degrade 정책 + core 안전 매도 독립(A-5 미구현, 별도 트래킹)으로 신규 매매 중단으로 영향 한정. big-bang 마이그레이션(R-5) 은 본 batch 가 REQ-022/025 동시 deliver 로 회피.
- DATA(비차단): 실 RabbitMQ broker 인스턴스/자격증명 provision 은 delivery/운영 작업 — verification 은 stub `InMemoryTransport` 로 충분 검증 가능하며 실 broker 미가용은 PG/KIS 와 동일하게 게이트 skip 으로 분리한다.
@@ -1,59 +0,0 @@
# BAT-007 Planning
- Status: draft
- Batch: BAT-007
- Profile: standard
## Included REQ
- REQ-021 (Integration): messaging 토폴로지 + 메시지 계약 매핑
- REQ-022 (Integration): messaging transport 포트 + RabbitMQ 어댑터 + stub + sync+thread daemon
- REQ-023 (Exception): messaging delivery 신뢰성 (effectively-once + durable + DLQ + publisher confirms)
- REQ-024 (Non-Functional): RabbitMQ 가용성 degrade 정책
- REQ-025 (Interface): HTTP `/v1` API 완전 제거 (healthz 포함, HTTP 표면 0)
- REQ-026 (Documentation): messaging 레이어 baseline 문서 정합
## Delivery Plan
- 의존 순서대로 구현한다.
1. REQ-021: `trex_core/adapters/messaging/topology.py`(또는 등가) 에 exchange/queue/binding/routing-key 정의. 매핑 원칙(view·strategy_id payload, event routing key)을 헬퍼로 고정.
2. REQ-022: `trex_core/application/messaging.py` Protocol(publish/subscribe/RPC), `adapters/messaging/in_memory.py`(결정론 stub), `adapters/messaging/rabbitmq.py`(pika BlockingConnection, 스레드별 channel + `add_callback_threadsafe`, 자체 재연결 백오프). `runtime/config.py` 에 `PortfolioHubConfig` 옆에 `RabbitMQConfig(url, timeout)` 추가, `TREX_RABBITMQ_URL` env 로딩.
3. REQ-025: `runtime/daemon.py` 에서 uvicorn import/실행과 FastAPI base 제거, `_execution_loop`/`_ledger_loop` 옆에 `_intent_rpc_consumer`/`_snapshot_rpc_consumer` 스레드 추가. `runtime/config.py` 의 `TREX_API_HOST`/`TREX_API_PORT` 폐기. `trex_core/adapters/api/` 전체 트리 + 해당 테스트 제거. `pyproject.toml` 의 `fastapi`/`uvicorn` 의존 제거.
4. REQ-023: consumer 에 manual ack-after-postgres-commit + correlation_id dedup (intent_log 또는 dedicated dedup store), 단일 consumer + prefetch=1, 이벤트 publisher 에 publisher confirms + DLQ exchange 라우팅, per-strategy durable queue + TTL/max-length.
5. REQ-024: 어댑터의 connection 상태 enum(`connected/disconnected/reconnecting`) 노출 + application 레이어 broker-down 분기(스냅샷 RPC timeout → 매매 보류, intent publish 실패 → 거부), 버퍼링 금지 회귀 가드 테스트.
6. REQ-026: REQ-021~025 구현이 안정화된 뒤 `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 3문서를 messaging 레이어로 정합 (HTTP `/v1` 섹션 제거, messaging 섹션 신설, sync+thread 런타임 반영).
- 데몬 reconcile 시점은 변경하지 않는다(REQ-014 broker reconcile, REQ-019 ledger outbox 처리 그대로 유지).
## Dependencies
- Internal:
- REQ-022 는 REQ-021 의 토폴로지 정의 선행.
- REQ-023 은 REQ-022 의 어댑터 ack/publish 경로에 통합.
- REQ-024 는 REQ-022 의 connection 상태 노출에 의존.
- REQ-025 는 REQ-022 의 sync+thread 런타임이 동시에 들어가야 통신 단절(R-5) 회피.
- REQ-026 은 REQ-021~025 구현 결과에 의존.
- 기존 자산: BAT-002 의 `IntentService`/`SlotService` 호출 경계, BAT-004 의 broker execution + ledger outbox 스레드, BAT-006 의 ledger writer 스레드는 그대로 재사용한다.
- dcy-003 의 멱등키(`correlation_id` 패턴)와 `intent_log` durable dedup 게이트(REQ-007/intent_log) 를 RabbitMQ intent consumer 의 dedup 으로 재사용한다.
- External:
- RabbitMQ broker 인스턴스 + 자격증명(`amqp://user:pass@host:5672/vhost`) — `TREX_RABBITMQ_URL` env 로 주입. 실 broker 미가용 시 stub 검증 + RabbitMQ 통합 테스트는 게이트 skip.
- `pika>=1.3` 신규 의존성 추가, `fastapi`/`uvicorn` 의존 제거.
- 전략 모듈 측 RabbitMQ 클라이언트 전환(외부 작업) — 본 batch 는 trex-core 측 transport 만 제공한다. 전략 모듈 전환 일정과 cut-over 는 운영 release 단계 의제.
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 transport 포트 + 외부 어댑터 + 데몬 런타임 재구성(uvicorn 제거 + sync+thread, intent/snapshot RPC consumer 스레드 신설) + delivery 신뢰성 규약(ack 시점, dedup 게이트, DLQ, publisher confirms, TTL/max-length) + degrade 정책의 connection 상태 머신 + HTTP 어댑터 전체 제거가 구조/인터페이스/런타임을 동시에 흔든다. 구현 전 토폴로지(exchange/queue/routing key/reply-to), ack-after-commit 시퀀스, 재연결 백오프 + degrade 상태 머신, 스레드별 채널 관리 규약을 design.md 에 고정해야 한다.
## Milestones
1. M1 — REQ-021: 토폴로지 선언(exchanges/queues/bindings/routing keys) + 매핑 헬퍼 + stub 어댑터 RPC/event 골격.
2. M2 — REQ-022: `MessagingTransport` 포트 + `InMemoryTransport`(stub) + `RabbitMQTransport`(pika 동기) + `RabbitMQConfig` + `TREX_RABBITMQ_URL` 로딩.
3. M3 — REQ-025: `adapters/api/` 제거 + `runtime/daemon.py` 의 uvicorn/FastAPI 제거 + intent/snapshot RPC consumer 스레드 신설 + `pyproject.toml`/`.env.example` 정리.
4. M4 — REQ-023: ack-after-commit + correlation_id dedup + 단일 consumer prefetch=1 + 이벤트 publisher confirms + DLQ + per-strategy durable queue + TTL/max-length.
5. M5 — REQ-024: connection 상태 노출 + application broker-down 분기 + 회귀 가드(버퍼링 0, 복구 후 자동 재생 0).
6. M6 — REQ-026: baseline 3문서 정합 + 변경 이력 추가.
7. M7 — 통합 verification: stub 어댑터 RPC/이벤트 lifecycle, dedup, durable+catch-up, conflict/DLQ, degrade 분기, HTTP 표면 0(포트/grep), 도메인 import 스윕, baseline 문서-코드 정합 리뷰.
## Risks
- R-1 (High, dcy-006 R-3): 가용성 결합 — RabbitMQ 가 SPOF 성격, HTTP fallback 없음. 대응: REQ-024 degrade 정책으로 신규 매매 중단으로 영향 한정. **A-5 (core 자율 안전 매도) 가 미구현 상태**이므로 운영 release 전에 broker outage 시 열린 포지션 처리 절차(운영 도구 또는 수동 SQL)를 별도 트래킹.
- R-2 (Medium, dcy-006 R-5): big-bang 마이그레이션 — HTTP 제거 시점과 전략 모듈 MQ 전환이 어긋나면 통신 단절. 대응: REQ-021/022/023/025 한 batch + stub 사전 검증 + release 단계에서 cut-over 순서(전략 모듈 → trex-core release) 합의.
- R-3 (Medium): 멱등 dedup store 선택 — intent_log(BAT-002, REQ-007) 를 그대로 dedup 게이트로 재사용할지, RabbitMQ consumer 전용 dedup table 을 새로 둘지. 대응: design 단계에서 결정 후 ack 시퀀스에 반영. 기본 추정은 intent_log 재사용(dcy-006 OQ-3 의 dcy-003 correlation_id idempotency 명시).
- R-4 (Medium): pika 자동 재연결 미지원으로 백오프 + 채널 재선언 + consumer rebind 를 직접 구현해야 함. 잘못 짜면 메시지 유실/중복 위험. 대응: design 단계에서 재연결 상태 머신 + 재선언 책임 명시 + 회귀 테스트.
- R-5 (Low): pika BlockingConnection 의 thread-safety 미보장 — 스레드별 connection/channel 또는 `add_callback_threadsafe` 규약을 design 으로 고정하고 stub/실 어댑터 둘 다 회귀 테스트.
- R-6 (Low): 실 RabbitMQ 인프라 미가용 — verification evidence 가 stub 수준에 집중될 수 있음. 대응: PG/KIS 와 동일하게 `TREX_RABBITMQ_TEST_URL` 게이트로 분리해 미가용 시 skip 명시. 실 통합 evidence 는 운영 단계에서 보강.
- R-7 (Low): dcy-005(스냅샷 read 계약) 가 아직 REQ 로 정규화되지 않은 상태에서 본 batch 의 snapshot RPC 가 먼저 도입됨. 응답 셰이프는 stub 수준에서 잠정 정의하고, dcy-005 REQ 가 확정되면 contract 정합을 후속 batch 에서 보강.
@@ -1,104 +0,0 @@
# BAT-007 Verification
- Status: approved
- Batch: BAT-007
## Acceptance Mapping
- REQ-021:
- AC-1: 데몬 기동 시 `trex.snapshot.req`, `trex.intent.req` 큐와 `trex.events` 토픽 익스체인지 선언 evidence (stub + 실 RabbitMQ).
- AC-2: 스냅샷 RPC 응답 logical 필드 + `correlation_id` 일치 evidence.
- AC-3: intent admission RPC 가 admission 응답을 reply-to 로 반환하고 체결/종료는 후속 이벤트로 발행되는 2단계 evidence.
- AC-4: strategy_id=A 이벤트가 `*.A.#` 바인딩 queue 만 수신 evidence (stub + 실 RabbitMQ).
- AC-5: 요청 메시지 payload 검사로 `view`/`strategy_id` 가 payload 에 있고 routing key 에 없음 evidence.
- REQ-022:
- AC-1: `InMemoryTransport` stub publish/subscribe/RPC 외부 broker 없이 동작 evidence.
- AC-2: `RabbitMQTransport` 가 `TREX_RABBITMQ_URL` 로 connection/channel 열고 송수신 evidence (게이트 통과, 실 broker 실행됨).
- AC-3: `Daemon` lifecycle 에서 intent/snapshot RPC + execution/ledger 스레드 동시 동작 + graceful shutdown(connection close 포함) evidence. `uvicorn.run` 의존 0 evidence (정적 import 가드).
- AC-4: connection drop 후 백오프 재연결 + degrade 정책에 따른 신규 처리 중단 evidence (재연결 코드 + stub 가용성 토글 회귀 가드).
- AC-5: `tests/domain/test_domain_import_sweep.py` 가 `pika`/`adapters.messaging` 0 import 확인 (NFR-1).
- REQ-023:
- AC-1: 동일 `correlation_id` intent 메시지 2회 처리 → 슬롯 전이 1회 + dedup evidence (NFR-2).
- AC-2: ack-after-commit 시퀀스 + intent_log durable dedup 으로 재전달 시 추가 전이 0 evidence (간접: dedup 테스트가 ack 순서를 전제로 통과).
- AC-3: intent 큐 토폴로지가 durable + DLQ pointer + prefetch=1 검사 evidence (실 broker PRECONDITION_FAILED 검사로 확인).
- AC-4: per-strategy durable queue 의 catch-up 동작은 토폴로지 args (`x-message-ttl`/`x-max-length`/`x-overflow=drop-head`/`x-dead-letter-exchange`) + 실 broker 의 per-strategy filter 테스트로 evidence.
- AC-5: 이벤트 페이로드에 `sequence_at` 단조 토큰 포함 evidence (테스트 단언).
- AC-6: publisher confirms + UnroutableError → DLQ 라우팅 코드 + stub DLQ 수집기 evidence.
- AC-7: TTL/max-length drop-oldest 는 토폴로지 args 선언으로 evidence (실 broker queue arguments 일치).
- REQ-024:
- AC-1: broker 불가 상태에서 publish/RPC 시 `TransportUnavailableError` 즉시 실패 + 슬롯 전이 0 evidence.
- AC-2: 로컬 버퍼링 없음 — `MessagingTransport` 인터페이스에 enqueue 가 없음 + 어댑터 코드 review.
- AC-3: `transport.health()` enum 노출 + 어댑터 `_mark_disconnected` 상태 머신 evidence.
- AC-4: A-5 정책 contract 가 REQ-024 Notes 와 BAT-007 implementation/verification Remaining Risks 에 명시 evidence.
- AC-5: 어댑터에 outbox/버퍼 없음 (코드 review) + 발행 실패가 슬롯 전이를 롤백하지 않음 evidence.
- REQ-025:
- AC-1: `tests/runtime/test_daemon_no_http_surface.py::test_adapters_api_tree_does_not_exist` + `::test_no_fastapi_or_uvicorn_in_trex_core_source` evidence.
- AC-2: 데몬 import 가드 (`test_daemon_module_has_no_fastapi_or_uvicorn_import`) evidence.
- AC-3: `test_config_env_no_longer_references_api_host_or_port` evidence.
- AC-4: `test_pyproject_dependencies_have_no_fastapi_or_uvicorn` evidence.
- AC-5: dcy-002 헤더 transport supersede 메모 + dcy-005 가 이미 dcy-006 cross-reference 갖추고 있음 evidence.
- AC-6: `Daemon` 클래스는 `signal.SIGINT/SIGTERM` 만 처리하고 HTTP 포트 바인딩 없음 (코드 review).
- REQ-026:
- AC-1: 3 baseline 문서 messaging 섹션 신설 + `Interface Summary`/`Covered Entry Points`/구조 트리에 반영 evidence.
- AC-2: baseline ↔ 코드 정합 manual review evidence.
- AC-3: 3 baseline 문서 Change Log 에 2026-06-05 BAT-007 항목 evidence.
- AC-4: `docs/interface-contract.md` 의 HTTP `/v1` 섹션 제거 + `docs/project-structure.md` 트리에서 `adapters/api/` 제거 evidence.
## Evidence
### Test
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" .venv/bin/python -m pytest -q`
- 결과: `135 passed, 12 skipped` (PG/KIS 게이트만 skip; 실 RabbitMQ 통합 7건 모두 실행/통과).
- REQ-021 AC-1: `tests/adapters/messaging/test_rabbitmq_transport.py::test_declare_topology_creates_core_entities` 가 두 번 declare 후 멱등 + `TransportHealth.CONNECTED` 확인.
- REQ-021 AC-2: `tests/application/test_messaging_snapshot_consumer.py::test_snapshot_returns_all_slots_and_capacity` 응답 셰이프 확인 + `tests/adapters/messaging/test_rabbitmq_transport.py::test_rpc_roundtrip` 가 `correlation_id` 일치 확인.
- REQ-021 AC-3: `tests/application/test_messaging_intent_consumer.py::test_buy_admission_returns_admitted_and_transitions_slot` + `tests/application/test_messaging_event_publisher.py::test_fill_emits_slot_filled_event_with_routing_key_and_state` 의 조합으로 admission 동기 + fill 비동기 이벤트 분리 확인.
- REQ-021 AC-4: `tests/application/test_messaging_event_publisher.py::test_event_for_strategy_a_does_not_reach_strategy_b_queue` + `tests/adapters/messaging/test_rabbitmq_transport.py::test_event_publish_and_per_strategy_consume` (실 RabbitMQ).
- REQ-021 AC-5: `tests/adapters/messaging/test_topology.py::test_event_routing_key_format` + `tests/adapters/messaging/test_topology.py::test_strategy_queue_name_and_binding_pattern`.
- REQ-022 AC-1: `tests/adapters/messaging/test_in_memory_transport.py` 5종.
- REQ-022 AC-2: `tests/adapters/messaging/test_rabbitmq_transport.py` 5종 (모두 실 broker 실행 통과).
- REQ-022 AC-3: `tests/runtime/test_daemon_no_http_surface.py::test_daemon_module_has_no_fastapi_or_uvicorn_import` + `Daemon` 4 스레드 + close 코드 review.
- REQ-022 AC-4: `RabbitMQTransport._sleep_backoff` + `_mark_disconnected` 코드 + `tests/adapters/messaging/test_in_memory_transport.py::test_unavailable_transport_raises_on_publish_and_rpc` degrade 분기 회귀.
- REQ-022 AC-5: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` (`pika`/`starlette` 포함 금지 prefix 갱신).
- REQ-023 AC-1: `tests/application/test_messaging_intent_consumer.py::test_duplicate_correlation_id_dedups_via_intent_log` + `tests/adapters/messaging/test_rabbitmq_intent_e2e.py::test_intent_admission_rpc_dedups_duplicate_correlation_id_over_rabbitmq` (실 RabbitMQ).
- REQ-023 AC-2: 위 dedup 테스트가 Postgres durable commit 전제(intent_log) 위에서 통과 — ack-after-commit 순서 evidence. `Daemon._intent_rpc_loop` 가 manual ack-after-handler 사용.
- REQ-023 AC-3: `tests/adapters/messaging/test_rabbitmq_intent_e2e.py::test_intent_queue_topology_uses_dlq_pointer` 가 PRECONDITION_FAILED 로 큐 argument(durable + DLQ pointer) 일치 확인.
- REQ-023 AC-4: `topology.STRATEGY_QUEUE_ARGS` (`x-message-ttl=3.6e6`, `x-max-length=10000`, `x-overflow=drop-head`, `x-dead-letter-exchange`) + per-strategy filter 테스트가 broker 단 필터링 확인.
- REQ-023 AC-5: `tests/application/test_messaging_event_publisher.py::test_fill_emits_slot_filled_event_with_routing_key_and_state` 가 `sequence_at` 포함 확인.
- REQ-023 AC-6: `RabbitMQTransport.publish_event` 의 `UnroutableError` → `_publish_dlq` + `_on_message` 의 handler exception NACK(requeue=False) + `tests/adapters/messaging/test_in_memory_transport.py::test_publish_event_with_no_subscriber_routes_to_dlq` evidence.
- REQ-023 AC-7: `STRATEGY_QUEUE_ARGS["x-overflow"] == "drop-head"` (`tests/adapters/messaging/test_topology.py::test_strategy_queue_args_have_ttl_and_max_length`) + 스냅샷 reconcile 경로(snapshot RPC handler) 존재.
- REQ-024 AC-1: `tests/adapters/messaging/test_in_memory_transport.py::test_unavailable_transport_raises_on_publish_and_rpc` + `tests/application/test_messaging_event_publisher.py::test_publish_failure_does_not_break_execution_loop`.
- REQ-024 AC-2: `MessagingTransport` Protocol 에 outbox/enqueue 가 없음 + 어댑터 코드 review (publish 실패 → 즉시 raise/DLQ, 로컬 보관 0).
- REQ-024 AC-3: `TransportHealth` enum + `InMemoryTransport.set_available`/`RabbitMQTransport._mark_disconnected` 코드 + AC-1 테스트가 disconnected 상태 노출 검증.
- REQ-024 AC-4: REQ-024 본문 Notes + BAT-007 implementation/verification Remaining Risks 에 명시.
- REQ-024 AC-5: publisher 실패 시 슬롯 상태 유지 테스트(`test_publish_failure_does_not_break_execution_loop`).
- REQ-025 AC-1~4: `tests/runtime/test_daemon_no_http_surface.py` 5종 모두 통과.
- REQ-025 AC-5: `docs/discovery/dcy-002_…md` 헤더 supersede 메모 + `docs/discovery/dcy-005_…md` 가 이미 dcy-006 cross-reference 보유.
- REQ-025 AC-6: `Daemon.run` 의 signal handler + 포트 바인딩 0(`ss -ltn` 으로 데몬 기동 후 확인 가능; 본 batch 는 import 가드로 충분 — 실 environment 에는 별도 운영 절차).
- REQ-026 AC-1/AC-3: `docs/interface-contract.md` / `docs/runtime-flows.md` / `docs/project-structure.md` 모두에 messaging 섹션 + Change Log 2026-06-05 BAT-007 항목 추가됨.
- REQ-026 AC-2: 본 verification 의 Manual 섹션 + code-search evidence.
- REQ-026 AC-4: `grep "HTTP \`/v1\`" docs/interface-contract.md` → 제거 안내 1건만 잔존(invalidate notice). 트리에서 `adapters/api/` 제거됨.
### Manual
- baseline ↔ 코드 정합 리뷰 (REQ-026 AC-1/AC-2):
- `docs/interface-contract.md` 의 `Interface: RabbitMQ messaging` 토폴로지/패턴/payload/Reliability/Degrade 표가 `trex_core/adapters/messaging/topology.py` 의 상수와 `trex_core/adapters/messaging/rabbitmq.py` 의 publish/consume 동작, `trex_core/application/messaging.py` 의 `MessagingTransport` Protocol 시그니처와 정합.
- `docs/runtime-flows.md` 의 `데몬 lifecycle`/`스냅샷 RPC`/`intent admission RPC`/`이벤트 발행·구독`/`broker degrade 분기` 흐름이 `trex_core/runtime/daemon.py:Daemon` 클래스(setup/start/shutdown + 4 worker) + `trex_core/application/execution.py:_emit_event` + `trex_core/application/messaging_handlers.py` 와 정합.
- `docs/project-structure.md` 디렉토리 트리에서 `trex_core/adapters/api/` 제거 + `trex_core/adapters/messaging/` 추가 + `trex_core/application/messaging.py`/`messaging_handlers.py` 추가 + daemon 의 4 스레드 구성 반영. 실제 디렉토리 트리(`ls trex_core/adapters/`, `ls tests/adapters/`) 와 일치.
- code-search (REQ-022 AC-5, REQ-025 AC-1):
- `grep -r "import httpx\|from httpx" trex_core/domain/` → 0건.
- `grep -r "import pika\|from pika" trex_core/domain/` → 0건.
- `grep -r "fastapi\|uvicorn\|starlette" trex_core/` → 0건.
- `find trex_core -path '*/adapters/api*'` → 0건.
- transport supersede cross-reference (REQ-025 AC-5):
- `docs/discovery/dcy-002_…md` 헤더에 dcy-006/BAT-007 supersede 메모 추가됨.
- `docs/discovery/dcy-005_…md` 는 본 batch 작업 전부터 dcy-006 cross-reference 보유 (FR-1, A-3, # 9 참조 목록).
### Logs
- `.venv/bin/ruff check trex_core tests` → `All checks passed!`.
- RabbitMQ 통합 evidence: `127.0.0.1:5672` admin/admin 으로 7건 통합 테스트(`test_rabbitmq_transport.py` 5 + `test_rabbitmq_intent_e2e.py` 2) 모두 PASSED. 큐/이벤트는 매 테스트가 uuid prefix 로 격리되어 잔존 0.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음.
- Notes:
- core 자율 안전 매도(A-5, dcy-006) 는 본 batch 범위 외 — release 전에 운영 절차 (broker 장기 outage 시 열린 포지션 처리) 문서화 또는 별도 Discovery 트래킹 권장.
- 전략 모듈 측 RabbitMQ 클라이언트 전환 + cut-over 순서(전략 모듈 → trex-core) 는 release 단계 의제.
@@ -1,62 +0,0 @@
# BAT-008 Design
- Status: draft
- Batch: BAT-008
## Architecture Summary
- 신규 `trex_core/adapters/portfolio/` read 계층을 추가해 portfolio-hub universe 를 가져온다. 스냅샷은 in-memory store 에 유지하되, pause overlay 만 Postgres 에 영속한다. warning 은 자동·임시 overlay 로 in-memory 에만 유지하고 다음 정합 성공 시 자동 해제한다. daemon 에 스케줄러 스레드를 추가해 일일 refresh 를 수행하고, BAT-007 의 snapshot RPC consumer 핸들러를 확장해 선별·마스킹·envelope 산출 로직을 연결한다.
## Changed Areas
- Docs: docs/interface-contract.md, docs/runtime-flows.md, docs/project-structure.md, docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md
- Modules:
- `trex_core/application/portfolio.py` (신규): `PortfolioHubReadPort` Protocol + 스냅샷 선별 서비스 + masking 합성 로직
- `trex_core/adapters/portfolio/` (신규): `hub_read.py`(실 어댑터), `stub_read.py`(결정론 stub)
- `trex_core/adapters/portfolio/pause_store.py` (신규): Postgres `snapshot_pause` 테이블 CRUD
- `trex_core/adapters/messaging/snapshot_handler.py` (확장): snapshot RPC 핸들러에 선별/envelope/degraded 로직 연결
- `trex_core/runtime/daemon.py` (확장): `_snapshot_scheduler` 스레드 추가, pause/resume RPC 제어 핸들러 연결
- `trex_core/runtime/config.py` (확장): `TREX_SNAPSHOT_SCHEDULE_KST` env, daemon_id 토큰 도출·캐시
- `trex_core/adapters/broker/` (확장): `BrokerPort.get_balance()` 메서드 추가 (KIS + paper stub)
- Interfaces:
- RabbitMQ `trex.snapshot.req` RPC: `{strategy_key?: str, view?: "all"|"buyable"|"held"}` → `{assets: [], view: str, available_slots: int, degraded: bool, reason: str|null}`
- CLI: `trex-core pause <portfolio_id>` / `trex-core resume <portfolio_id>`
- RPC 제어 메시지 (BAT-007 채널): `{action: "pause"|"resume", portfolio_id: str}` 핸들러
## Key Decisions
1. **스냅샷 store 분리**: in-memory `SnapshotStore`(universe·현금·포지션·warning) + Postgres `snapshot_pause`(pause). warning 은 process 재시작 시 초기화(일시적·자동), pause 는 Postgres 에서 복원(영속·수동). 이 분리가 REQ-031/032 의 "자동 warning vs 수동 pause" 시맨틱을 명확히 구현한다.
2. **masking 합성 순서**: `pending(slot 상태) OR warning(정합 실패) OR pause(수동)` → 셋 중 하나라도 true 면 마스킹. 적용은 view 집합 산출 후 균일 적용. 우선순위: 슬롯 overlay > portfolio-hub 보유 여부.
3. **daemon_id 도출**: `PortfolioHubReadPort` 초기화 시 portfolio-hub `GET /v1/user_info` 1회 호출 → 응답 `daemon_id` 추출·캐시. `TREX_PORTFOLIO_HUB_TOKEN` 자격증명 재사용(REQ-027 A-1). (정정: REQ-027 CHG-04 — `/v1/auth/whoami` 부재로 `/v1/user_info` 로 변경)
4. **strategy_key 필터**: 요청 payload 의 `strategy_key`(str) 를 자산의 `active_strategy_id` 와 **대소문자 구분 정확 일치**(`==`)로 필터링. 생략 시 필터 없음.
5. **degraded 분기**:
- `portfolio_hub_unavailable`: 스케줄러 fetch 또는 RPC handler 시점에 portfolio-hub 에 연결 불가.
- `snapshot_fetch_failed`: fetch 시도 자체는 성공했으나 응답 파싱/검증 실패.
- `cash_position_stale`: 옵션 D 갱신 실패(ledger write 응답에 `*_after` 없거나 stale 감지).
- 각 경우 sanitized message 포함, raw exception/credentials 노출 금지.
6. **스케줄러 스레드 snapshot 교체**: 일일 fetch 완료 시 `SnapshotStore` 를 **immutable swap**(새 dict 할당 후 atomic reference 교체) 으로 교체. snapshot RPC handler 가 구버전·신버전 혼합 읽기 없도록 보장. warning/pause overlay 는 swap 후 재적용.
7. **KIS D-2 0수량 잔고 처리**: `hldg_qty == 0` 인 잔고 항목은 **미보유**로 취급(잔고 없음). 트리거: 당일 전량매도 후 KIS 잔고가 D-2 까지 0으로 남는 케이스. 이 항목이 symbol 집계 결과 0수량이면 portfolio-hub 포지션 0과 일치(warning 없음).
8. **BrokerPort 잔고조회 확장**: `get_balance() → list[BalanceItem]` 메서드를 `BrokerPort` Protocol 에 추가. KIS 어댑터: `TTTC8434R`(실전)/`VTTC8434R`(모의) 분기. paper stub: 결정론 고정 잔고 반환. 기존 REQ-011/012 어댑터에 메서드 추가(인터페이스 확장, 기존 동작 불변).
9. **옵션 D 연결점**: ledger write 응답(BAT-006 `LedgerAdapter`) 이 `cash_balance_after`/`asset_quantity_after` 를 반환하면 `SnapshotStore.update_cash_position(asset_id, cash, qty)` 호출. portfolio-hub#8 배포 전 응답에 해당 필드가 없으면 갱신 skip(graceful).
## Edge Cases
- portfolio-hub 불가 **at startup**: `SnapshotStore` 비어 있음 + `degraded=true`. 전략 요청에 `assets: [], degraded: true, reason: "portfolio_hub_unavailable"` 반환. 신규 매매 보류(전략 모듈 책임).
- `strategy_key` 필터 결과가 빈 배열: `assets: [], degraded: false`. 필터 미매치는 degraded 가 아님.
- `available_slots=0` + `view=buyable`: `assets: [], available_slots: 0, degraded: false`.
- 같은 자산에 warning + pause 동시: 둘 다 마스킹 적용(OR), 어느 쪽이 먼저 해제되든 나머지 하나가 남아있으면 여전히 마스킹.
- pause 중 일일 refresh: universe·포지션 갱신 후 pause overlay 재적용(pause 불변).
- holding 슬롯 자산의 수량 표기: snapshot baseline 값 사용. holding 중 ledger flush 지연으로 실 포지션과 갭이 있을 수 있음 — 옵션 D 갱신이 완료되면 write 응답으로 즉시 보정.
## Architecture Impact
- both (project-structure: `adapters/portfolio/` 신규 계층, runtime-flows: 스케줄러 스레드 + 일일 fetch + masking 합성 흐름 + 옵션 D ledger hook)
## Reference Doc Update Plan
- project-structure.md: `adapters/portfolio/`(read 어댑터·stub·pause store) 신규 섹션, `application/portfolio.py` 포트·선별 서비스, 스케줄러 스레드 추가.
- runtime-flows.md: 일일 스냅샷 fetch 흐름, 정합 검증 흐름, pause/resume 흐름, snapshot RPC 응답 산출 흐름(masking 합성 포함), 옵션 D ledger write hook 흐름.
- interface-contract.md: `trex.snapshot.req` RPC 요청/응답 envelope 계약, pause/resume 제어 메시지 계약, `PortfolioHubReadPort` 포트 시그니처.
@@ -1,69 +0,0 @@
# BAT-008 Implementation
- Status: complete (M1-M8; M9 옵션 D 는 portfolio-hub#8 배포 후)
- Batch: BAT-008
## Plan Summary
- M1-M8 마일스톤 모두 완료. M9(REQ-030 옵션 D `*_after` 갱신)는 portfolio-hub#8 배포 후 별도 실행. `SnapshotStore.update_cash_position` wiring point 가 이미 노출되어 있어 배포 후 LedgerWriter 호출 1줄 추가로 활성화 가능.
## Changed Files
- 신규
- `trex_core/application/portfolio.py` — `PortfolioHubReadPort` Protocol, `PortfolioItem`, `PortfolioHubUnavailableError`, `View`/`DegradedReason` enum, `SnapshotStore`(swap/warning/option-D/degraded), `PauseStore` Protocol + `InMemoryPauseStore`, `SnapshotEnvelope` + `build_snapshot_envelope`.
- `trex_core/application/reconciliation.py` — `reconcile_positions(store, broker)` symbol-aggregate 비교 + warning 자동 set/clear.
- `trex_core/application/snapshot_scheduler.py` — `SnapshotScheduler.run_once()` (fetch + swap + reconcile) + `run_forever(stop, tick_sec)` (boot 즉시 1회 + KST 시각 도래 시 1회).
- `trex_core/adapters/portfolio/__init__.py`, `hub_read.py`(`PortfolioHubReadAdapter` httpx — whoami + list_portfolios), `stub_read.py`(`StubPortfolioRead` 결정론), `pause_store.py`(`PostgreSQLPauseStore` upsert/delete).
- `trex_core/adapters/persistence/sql/003_create_snapshot_pause.sql` — `snapshot_pause(portfolio_id PK, actor, created_at, updated_at)`.
- `trex_core/runtime/cli.py` + `trex-core` console_script — `pause/resume/paused` 서브커맨드.
- 테스트: `tests/adapters/portfolio/{test_stub_read,test_hub_read,test_postgresql_pause_store}.py`, `tests/application/{test_snapshot_selection,test_snapshot_handler,test_reconciliation,test_snapshot_scheduler,test_pause_handler}.py`.
- 수정
- `trex_core/application/broker_port.py` — `BalanceItem` 데이터클래스 + `BrokerPort.get_balance() -> list[BalanceItem]` 메서드 추가.
- `trex_core/adapters/broker/paper.py` — `get_balance()`: fill aggregate (buy − sell) per symbol, 0수량 drop.
- `trex_core/adapters/broker/kis.py` — `get_balance()`: `TTTC8434R`/`VTTC8434R` `inquire-balance` 호출, `output1[*].pdno + hldg_qty`, 0수량 drop (D-2 leftover 처리, Decision 7).
- `trex_core/adapters/messaging/topology.py` — `PAUSE_REQ_QUEUE = "trex.pause.req"` 추가.
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology()` 가 `PAUSE_REQ_QUEUE` 도 선언.
- `trex_core/application/messaging_handlers.py` — `make_snapshot_rpc_handler` 에 `snapshot_store`/`pause_store`/`default_total_slots` 인자 추가 (BAT-008 envelope path). 인자 미제공 시 BAT-007 stub-shape 폴백. 신규 `make_pause_rpc_handler(pause_store)`.
- `trex_core/runtime/config.py` — `SnapshotScheduleConfig` + `TREX_SNAPSHOT_SCHEDULE_KST`/`TREX_SNAPSHOT_TICK_SEC` env 로딩, `DaemonConfig.snapshot_schedule` 필드.
- `trex_core/runtime/daemon.py` — `_build_portfolio_read(config)`, `_parse_kst_time`. `Daemon` 에 `snapshot_store`/`pause_store`/`portfolio_read`/`snapshot_scheduler` 인스턴스 추가. 신규 worker thread `_pause_rpc_loop`, `_snapshot_scheduler_loop`. `_snapshot_rpc_loop` 가 BAT-008 envelope handler 사용. `start()` 의 worker 목록을 6개로 확장.
- `pyproject.toml` — `trex-core = "trex_core.runtime.cli:main"` console_script 추가.
- `.env.example` — `TREX_SNAPSHOT_SCHEDULE_KST=07:30`, `TREX_SNAPSHOT_TICK_SEC=60` 추가.
- `tests/conftest.py` — `_PG_TABLES` 에 `snapshot_pause` 추가.
- `tests/adapters/persistence/test_migration_and_drift.py` — `_EXPECTED_TABLES` 에 `snapshot_pause` 추가.
- baseline 문서 갱신 — `docs/runtime-flows.md`/`docs/interface-contract.md`/`docs/project-structure.md` 에 BAT-008 섹션 + Change Log 추가.
## Execution Log
1. baseline 테스트 147 passed (PG + RabbitMQ 게이트 모두 가용) 확인.
2. M1 (REQ-027): `application/portfolio.py` 의 read 포트/DTO/store + `adapters/portfolio/{hub_read,stub_read}` + `pause_store.py` 작성. 단위테스트 + httpx MockTransport 기반 어댑터 contract 테스트 작성.
3. M2 (REQ-030 baseline): `snapshot_scheduler.py` 의 `SnapshotScheduler` + `run_once()` 작성. `runtime/config.py` 에 `SnapshotScheduleConfig` 추가, `runtime/daemon.py` 에 `_snapshot_scheduler_loop` 스레드 추가.
4. M3 (REQ-031): `BrokerPort.get_balance()` 확장 + paper/KIS 구현 + `application/reconciliation.py` 의 `reconcile_positions` 작성 + 단위테스트.
5. M4 (REQ-032): `003_create_snapshot_pause.sql` 마이그레이션 + `PostgreSQLPauseStore` + `make_pause_rpc_handler` + `runtime/cli.py` 작성. RPC handler / PG persistence / CLI smoke 모두 테스트.
6. M5 (REQ-029): `build_snapshot_envelope` 의 base(view) 산출 + masking OR 합성 + strategy_key 필터 작성 + 10개 시나리오 테스트.
7. M6 (REQ-028): `make_snapshot_rpc_handler` 를 envelope path 로 확장 (legacy stub-shape 폴백 유지 → BAT-007 회귀 0). degraded 신호 sanitization 포함.
8. M7 통합 검증: stub `SnapshotStore` + slot 도메인 + warning + pause 합성 시퀀스 테스트 + 도메인 import 스윕 회귀 통과.
9. M8 (REQ-033): baseline 3 문서(runtime-flows / interface-contract / project-structure) 에 BAT-008 섹션 + Change Log 추가.
10. End-to-end 운영 환경 검증: 실 PostgreSQL + 실 RabbitMQ 위에서 `trex-core-daemon` 기동 → snapshot RPC (view=all/buyable/held + strategy_key 필터) → pause/resume RPC → snapshot 재요청까지 1회 통과. portfolio-hub 서버 미가동 환경에서 `degraded=true reason=portfolio_hub_unavailable` 정상 반환 확인.
11. 최종 검증: `TREX_RABBITMQ_TEST_URL` + `TREX_POSTGRESQL_TEST_DSN` 게이트 모두 가용 상태에서 `pytest -q` → **185 passed / 1 skipped** (KIS sandbox 토큰 rate-limit 1건만 skip). `ruff check trex_core tests` → All checks passed.
## Validation
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" TREX_POSTGRESQL_TEST_DSN=$(grep TREX_POSTGRESQL_DSN .env | cut -d= -f2-) .venv/bin/python -m pytest -q`
- 결과: `185 passed, 1 skipped` (KIS sandbox 토큰 rate-limit).
- 명령: `.venv/bin/ruff check trex_core tests`
- 결과: `All checks passed!`.
- 추가 테스트 ~39 종:
- `tests/adapters/portfolio/test_stub_read.py` — 3 (REQ-027 AC-2)
- `tests/adapters/portfolio/test_hub_read.py` — 4 (REQ-027 AC-1/AC-4)
- `tests/adapters/portfolio/test_postgresql_pause_store.py` — 4 (REQ-032 AC-2)
- `tests/application/test_snapshot_selection.py` — 10 (REQ-029 AC-1~5, REQ-028 AC-1/AC-2/AC-3)
- `tests/application/test_snapshot_handler.py` — 4 (REQ-028)
- `tests/application/test_reconciliation.py` — 4 (REQ-031 AC-1/AC-2/AC-3)
- `tests/application/test_snapshot_scheduler.py` — 4 (REQ-030 AC-1, 옵션 D wiring point)
- `tests/application/test_pause_handler.py` — 5 (REQ-032 AC-1/AC-3/AC-4)
- 운영 환경 검증 (실 PG + RabbitMQ + `trex-core-daemon`):
- snapshot RPC envelope 모든 키 + no `price` 필드 + `degraded` 신호 확인.
- pause RPC → snapshot 마스킹 → resume RPC → snapshot 복귀 확인.
- view 3 모드 + strategy_key 필터가 envelope 응답에 반영됨.
## Remaining Risks
- **REQ-030 옵션 D (AC-2/AC-3)**: portfolio-hub#8 배포 대기. `SnapshotStore.update_cash_position` wiring point 는 이미 노출됨 — 배포 확인 후 LedgerWriter 의 ledger write 응답 소비 경로에서 `cash_balance_after`/`asset_quantity_after` 가 있을 때 호출 1줄 추가로 활성. 본 batch verification 에서는 deferred residual risk 로 기록.
- **KIS D-2 0수량 잔고 운영 검증**: 설계 Decision 7(0수량 drop) 은 paper broker 테스트로 회귀 가드되어 있으나, 실 KIS 환경에서 D-2 leftover 케이스를 한 번 관찰해 운영 노트화 권장.
- **scheduler 실 운영 검증**: 본 batch 의 검증은 `run_once()` 단위테스트 + boot 즉시 fire 운영 확인까지. `TREX_SNAPSHOT_SCHEDULE_KST` 시각 fire 는 실 데몬 운영 후 첫 07:30 KST 도래 시 관찰 필요.
- **portfolio-hub HTTP 통합 evidence**: `httpx.MockTransport` contract 테스트 + 실 데몬이 portfolio-hub 부재 시 `degraded` 정상 노출까지 검증. portfolio-hub 가용 환경에서 실 응답 round-trip 은 운영 단계 추가 evidence 로 보강 권장.
@@ -1,39 +0,0 @@
# BAT-008: portfolio-hub 스냅샷 read 레이어
- Status: released
- Profile: standard
- Source Discovery: dcy-005
- Included REQ: REQ-027, REQ-028, REQ-029, REQ-030, REQ-031, REQ-032, REQ-033
- Excluded REQ: 없음
## Summary
- dcy-005 스냅샷 read 슬라이스 전체를 하나의 delivery 단위로 묶는다. portfolio-hub read 포트/어댑터로 universe 를 가져오고(REQ-027), 일일 스케줄러로 스냅샷을 갱신하며(REQ-030), KIS 잔고조회로 포지션 정합을 검증해 warning 마스킹을 적용하고(REQ-031), 수동 pause/resume 제어를 Postgres 에 영속한다(REQ-032). 이 모든 overlay 를 합성해 slot 상태 기반 자산 선별·view·strategy_key 필터링으로 응답 집합을 산출하고(REQ-029), BAT-007 의 RabbitMQ snapshot RPC 채널로 전달하는 메시지 계약과 degraded 신호를 정의한다(REQ-028). 구현 완료 후 baseline 3문서를 정합한다(REQ-033).
## Scope
- In Scope:
- REQ-027: `PortfolioHubReadPort.get_portfolios(daemon_id)` 포트 + portfolio-hub read 어댑터(`managed_by_daemon_id` 스코프, `list_portfolios_v1`) + 결정론 stub. `daemon_id` 는 portfolio-hub 인증 토큰에서 도출·캐시(별도 `TREX_DAEMON_ID` 불필요). 각 자산: symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params(opaque). 도메인 순수성 import 스윕(NFR-1).
- REQ-028: snapshot RPC(`trex.snapshot.req`) 메시지 계약 — 요청(선택적 strategy_key·view), envelope 응답(`assets[]`·view·available_slots·degraded·reason). portfolio-hub 불가 시 `degraded=true` + machine-readable reason code + sanitized message(raw 예외 노출 금지).
- REQ-029: 응답 base 집합은 view(`all`/`buyable`/`held`)로 선택 후 마스킹(pending·warning·pause) 균일 적용. strategy_key 직교 필터. 최종 합성: `base(view) − mask(pending·warning·pause) − strategy_key 필터`. 슬롯 overlay 가 portfolio-hub 보유 여부보다 우선.
- REQ-030: 설정 가능한 시각(기본 07:30 KST)에 스케줄러가 universe 를 페치해 스냅샷(멤버십·전략파라미터·baseline 현금/포지션) 갱신. 체결 후 ledger write 응답의 `cash_balance_after`/`asset_quantity_after`(옵션 D)로 현금/포지션 갱신 — portfolio-hub#8 배포 후 착수.
- REQ-031: 스냅샷 페치 시 KIS 주식잔고조회(`TTTC8434R`/`VTTC8434R`) 수량 정확 일치 검증(symbol 집계 vs broker `hldg_qty`). 불일치 자산 warning 마스킹 + 다음 정합 성공 시 자동 해제.
- REQ-032: pause 단위 portfolio_id, CLI + dcy-006 RPC 제어 메시지 양방향. Postgres 영속(재시작/refresh 후에도 유지). 전역 마스킹, resume 시 다음 요청부터 즉시 복귀.
- REQ-033: `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 를 read 포트·스냅샷 계약·스케줄러·정합 검증 흐름에 맞게 갱신.
- Out of Scope:
- portfolio-hub write(ledger) 연동 — BAT-006(REQ-017~019) 소관; 본 batch 는 read 만.
- RabbitMQ transport 토폴로지/delivery 신뢰성 — BAT-007(REQ-021~026) 소관.
- 전략 모듈 측 RabbitMQ 클라이언트 구현(외부).
- REQ-030 옵션 D 현금/포지션 갱신 구현 — portfolio-hub#8 배포 전 대기.
- 강제 청산(kill-switch) — trex-core#3 별도 Discovery.
- 전략 taxonomy(declarative intent) — trex-core#1 별도 Discovery.
## 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-005 단일. 핵심 확정 결정: Option D(ledger write 응답 `*_after`, dcy-005 OQ-2) + degraded 신호(portfolio-hub 불가 → degraded=true + reason code, OQ-5) + view 3모드(all/buyable/held, OQ-11/12) + strategy_key(사람-읽기 안정 slug, OQ-9) + pause(portfolio_id 단위·Postgres 영속·즉시 복귀, OQ-10) + position 정합(수량 정확 일치·symbol 집계·KIS 잔고조회, OQ-8).
- **의존**: REQ-030 옵션 D 구현은 portfolio-hub#8 배포 대기. baseline 스케줄러(universe fetch)는 선구현 가능.
- **검증 순서 제약**: REQ-029 AC-5(warning·pause 마스킹) 검증은 REQ-031(warning 상태)과 REQ-032(pause 상태) 구현 완료 후 가능.
- **리스크 D-2**: KIS 당일 전량매도 후 잔고가 `hldg_qty=0` 으로 D-2 까지 남을 수 있음 → 0수량 잔고 처리 규칙 설계 단계 확정.
@@ -1,71 +0,0 @@
# BAT-008 Planning
- Status: draft
- Batch: BAT-008
- Profile: standard
## Included REQ
- REQ-027 (Integration): portfolio-hub read 포트 + 어댑터 + stub
- REQ-028 (Interface): 종목 스냅샷 read 메시지 계약 + envelope + degraded 신호
- REQ-029 (Interface): 자산 선별(slot overlay) + view 모드 + strategy_key 필터
- REQ-030 (Integration): 일일 스냅샷 스케줄러 + 옵션 D 현금/포지션 갱신
- REQ-031 (Exception): 포지션 정합 검증 + warning 마스킹
- REQ-032 (Interface): 런타임 pause/resume 제어
- REQ-033 (Documentation): 스냅샷 read 레이어 baseline 문서 정합
## Delivery Plan
- 의존 그래프 순서대로 구현한다.
1. **REQ-027** — `PortfolioHubReadPort` 포트(`trex_core/application/`) + portfolio-hub read 어댑터(`trex_core/adapters/portfolio/hub_read.py`) + 결정론 stub(`trex_core/adapters/portfolio/stub_read.py`). `runtime/config.py` 에 `daemon_id` 토큰 도출·캐시 로직 추가. 도메인 import 스윕 가드 확장.
2. **REQ-030 (baseline)** — 신규 스케줄러 스레드를 sync+thread daemon 에 추가(`_snapshot_scheduler` 스레드). `TREX_SNAPSHOT_SCHEDULE_KST`(기본 `07:30`) env 파라미터. universe fetch → 스냅샷 store 갱신. **옵션 D(`cash_balance_after`/`asset_quantity_after`) 갱신은 portfolio-hub#8 배포 후 별도 구현.**
3. **REQ-031** — BrokerPort 에 `get_balance() → list[BalanceItem(pdno, hldg_qty)]` 잔고조회 확장(KIS `TTTC8434R`/`VTTC8434R`, paper stub 결정론). 정합 검증 서비스: symbol 집계 vs broker per-symbol `hldg_qty` 수량 정확 일치, 불일치 자산 warning flag, 다음 정합 성공 시 자동 해제. 0수량 잔고 처리 규칙은 설계 단계 확정.
4. **REQ-032** — Postgres `snapshot_pause` 테이블(portfolio_id PK). CLI `trex-core pause <portfolio_id>` / `resume`. BAT-007 RPC 채널에 pause/resume 제어 메시지 핸들러 추가. daemon 재시작/일일 refresh 후에도 pause 영속.
5. **REQ-029** — 스냅샷 선별 서비스: `base(view)` 집합 산출 후 `mask(pending · warning · pause)` OR 합성 마스킹 후 `strategy_key` 직교 필터. SlotRepository(BAT-002) 재사용.
6. **REQ-028** — BAT-007 의 snapshot RPC consumer(`trex.snapshot.req`) 핸들러에 envelope 산출 로직 연결. degraded 분기: portfolio-hub 불가 탐지 → `degraded=true` + reason code + sanitized message. null 금지(`assets: []`).
7. **REQ-033** — REQ-027~032 구현 안정화 후 `docs/interface-contract.md`/`docs/runtime-flows.md`/`docs/project-structure.md` 정합.
8. **REQ-030 (옵션 D)** — portfolio-hub#8 배포 확인 후, ledger write 응답 소비 경로에 `cash_balance_after`/`asset_quantity_after` → 스냅샷 store 갱신 로직 추가. idempotent 갱신(duplicate write 처리).
## Dependencies
- Internal:
- REQ-027(read port) 선행 → REQ-028, REQ-029, REQ-030, REQ-031 모두.
- REQ-031(warning 상태), REQ-032(pause 상태) 선행 → REQ-029(마스킹 합성) 전체 검증 가능.
- REQ-029(선별 로직) 선행 → REQ-028(RPC handler 응답 산출).
- REQ-033 후행 → REQ-027~032 모두.
- BAT-002 `SlotRepository`(slot 상태) — REQ-029 slot overlay 재사용.
- BAT-004 `BrokerPort`(REQ-011/012) — REQ-031 잔고조회 확장.
- BAT-006 `LedgerPort`(REQ-017~019) — REQ-030 옵션 D ledger write hook.
- BAT-007 RabbitMQ snapshot RPC consumer(REQ-021/022) — REQ-028 핸들러 연결점.
- External:
- portfolio-hub `GET /v1/portfolios?managed_by_daemon_id=<id>` (`list_portfolios_v1`) — A-1 전제.
- **portfolio-hub#8** (`POST /v1/portfolios/{id}/ledger/trades` 응답에 `cash_balance_after`/`asset_quantity_after`) — REQ-030 옵션 D 의존. 배포 전까지 구현 대기.
- KIS 주식잔고조회(`TTTC8434R`/`VTTC8434R`, `GET /uapi/domestic-stock/v1/trading/inquire-balance`) — REQ-031. KIS 미가용 시 paper stub 결정론 fallback.
- RabbitMQ broker — BAT-007 transport 재사용(추가 provision 불필요).
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 `adapters/portfolio` 계층 도입(read port 추상화 + 어댑터 + stub 구조 결정), 스냅샷 store 설계(in-memory snapshot + Postgres pause overlay 분리), warning state 머신(자동 임시 vs pause 영속 구분), KIS D-2 0수량 잔고 처리 규칙, masking 합성 순서(pending·warning·pause OR 적용 방식), degraded 신호 분기 결정(portfolio_hub_unavailable / snapshot_fetch_failed / cash_position_stale), daemon_id 토큰 도출·캐시 방식, 스케줄러 스레드 통합 방식을 구현 전에 design.md 에 고정해야 한다.
## Milestones
1. M1 — REQ-027: `PortfolioHubReadPort` 포트 + read 어댑터 + stub + daemon_id 토큰 도출 + import 스윕.
2. M2 — REQ-030 (baseline): 스케줄러 스레드 + universe fetch + 스냅샷 store 갱신(옵션 D 제외).
3. M3 — REQ-031: `BrokerPort.get_balance()` 확장(KIS/paper) + 정합 검증 + warning 상태 자동 해제.
4. M4 — REQ-032: `snapshot_pause` Postgres 테이블 + CLI + RPC 제어 핸들러 + 영속 검증.
5. M5 — REQ-029: 스냅샷 선별 서비스(base집합 + masking OR합성 + strategy_key 필터).
6. M6 — REQ-028: snapshot RPC 핸들러 + envelope 산출 + degraded 분기.
7. M7 — 통합 verification: end-to-end snapshot RPC 흐름, masking 합성(pending·warning·pause), strategy_key 필터, degraded 주입, pause 영속, 도메인 import 스윕.
8. M8 — REQ-033: baseline 3문서 정합.
9. M9(후속) — REQ-030 옵션 D: portfolio-hub#8 배포 확인 후 ledger write 응답 `*_after` 소비 + idempotent 갱신.
## Risks
- R-1 (Medium): **portfolio-hub#8 배포 일정 불확실** — REQ-030 옵션 D 구현 대기. 대응: baseline 스케줄러(fetch만)를 먼저 deliver 하고 옵션 D 는 M9 로 분리. 배포 확인 전 스냅샷 현금/포지션은 일일 fetch baseline 으로만 유지.
- R-2 (Medium): **KIS D-2 0수량 잔고** — 당일 전량매도 후 잔고가 `hldg_qty=0` 으로 D-2 까지 남음(KIS 015 문서). 0수량을 보유로 볼지 미보유로 볼지 처리 규칙 미확정. 대응: design 단계에서 처리 규칙 확정(예: `hldg_qty=0` 인 항목은 잔고 없음으로 취급).
- R-3 (Medium): **masking 합성 경계 케이스** — 같은 자산에 warning·pause 동시 적용, pending 상태에서 resume 호출, holding 슬롯과 portfolio-hub 보유 수량 불일치(holding→ledger flush 갭). 대응: AC 별 테스트 시퀀스를 verification.md 에 명시.
- R-4 (Low): **portfolio-hub read API 인터페이스** — `list_portfolios_v1` 의 `managed_by_daemon_id` 스코프 지원 여부, `PortfolioItem` 응답 스키마 확정 필요(A-1 전제). 대응: stub 어댑터로 선검증, 실 통합은 게이트 분리.
- R-5 (Low): **스케줄러 스레드 충돌** — 스케줄러 fetch 중 snapshot RPC 요청이 도착하면 partial 상태 읽기 위험. 대응: design 단계에서 snapshot store 락 또는 immutable swap 전략 확정.
@@ -1,120 +0,0 @@
# BAT-008 Verification
- Status: approved
- Batch: BAT-008
## Acceptance Mapping
- REQ-027 (계약 정정 CHG-04 반영):
- AC-1: HTTP MockTransport 통합테스트에서 `{portfolios:[…]}` envelope 파싱 + `managed_by_daemon_id` 클라이언트측 필터 + `PortfolioItem` 항목 `symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params` 필드 + `price` 부재 확인. 실 list 응답에 전략 필드 없으면 None/`{}` 기본값.
- AC-2: 결정론 stub 가 네트워크 없이 고정 universe 반환 확인.
- AC-3: 도메인 import 스윕(`tests/domain/test_domain_import_sweep.py`)이 `pika`/`httpx`/`adapters.*` 0 import 회귀 가드.
- AC-4: HTTP MockTransport 가 `/v1/user_info` 호출 + Bearer 토큰 헤더 + `daemon_id` 도출 확인. 스킴 없는 base URL → `http://` 기본 적용. `TREX_DAEMON_ID` env 1차 출처 없음(코드 review).
- REQ-028:
- AC-1: envelope 응답이 정확히 `{assets, view, available_slots, degraded, reason, message}` 키 집합 + `assets` 가 항상 list(null 금지) 확인.
- AC-2: `SnapshotStore.set_degraded(...)` 후 envelope 가 `degraded=true reason=portfolio_hub_unavailable`. 실 운영 검증 — portfolio-hub 부재 환경에서 daemon 기동 후 snapshot RPC 가 같은 응답 반환.
- AC-3: 응답 항목에 `symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params` 만 존재, `price` 키 없음.
- REQ-029:
- AC-1: 같은 symbol holding + 다른 portfolio_id pending → `view=all` 에서 holding 자산만 포함, pending portfolio_id 마스킹.
- AC-2: `available_slots=0` + `view=buyable` → `assets=[]`.
- AC-3: `view=held` → holding 슬롯이 가리키는 portfolio_id 만 응답.
- AC-4: `strategy_key="target-value-v1"` → 일치 자산만 / 생략 시 마스킹 후 전체.
- AC-5: warning(`set_warning`) 또는 pause(`pause_store.pause`) 자산은 임의 view 에서 균일 마스킹.
- REQ-030:
- AC-1: `SnapshotScheduler.run_once()` 가 universe fetch + `SnapshotStore.swap` + reconcile 1회 수행 확인. 실 daemon boot 직후 동일 동작.
- AC-2 (옵션 D): **deferred — portfolio-hub#8 배포 후 evidence 수집**. `SnapshotStore.update_cash_position(pid, cash, qty)` wiring point 는 노출되어 있어 배포 후 LedgerWriter 의 ledger write 응답 소비 경로에서 호출 1줄 추가로 활성. 본 batch 에서는 wiring 단위테스트만 통과.
- AC-3 (옵션 D, duplicate idempotent): **deferred — portfolio-hub#8 배포 후 evidence 수집**.
- REQ-031:
- AC-1: hub symbol 집계 수량 ≠ broker `hldg_qty` → `SnapshotStore.set_warning(pid, True)` + envelope 에서 마스킹 확인.
- AC-2: 2 라운드 reconcile 시퀀스(불일치→일치)에서 warning 자동 해제 + 응답 복귀 확인.
- AC-3: paper broker `get_balance()` 가 fill aggregate 기반 결정론 잔고 반환 확인. 0수량 row drop 가드 포함.
- REQ-032:
- AC-1: `pause(pid)` 후 임의 view/strategy_key 요청에서 해당 portfolio_id 응답 마스킹.
- AC-2: PostgreSQL pause store 재인스턴스화(restart 시뮬레이션) 후에도 paused row 유지.
- AC-3: `resume(pid)` 후 다음 요청 envelope 에 복귀.
- AC-4: 동일 `PostgreSQLPauseStore` 를 CLI 와 RPC handler 가 공유 → CLI pause → RPC `action=list` 에 노출. 실 운영 검증 — daemon RPC pause → 같은 store 의 paused 목록 확인.
- REQ-033:
- AC-1: `docs/runtime-flows.md`/`docs/interface-contract.md`/`docs/project-structure.md` 모두에 BAT-008 섹션 + Change Log 항목 추가.
- AC-2: 베이스라인 ↔ 코드 정합 manual review — 본 verification 의 Manual 섹션 참조.
## Evidence
### Test
- 명령: `TREX_RABBITMQ_TEST_URL="amqp://admin:admin@127.0.0.1:5672/" TREX_POSTGRESQL_TEST_DSN=$(grep TREX_POSTGRESQL_DSN .env | cut -d= -f2-) .venv/bin/python -m pytest -q`
- 결과: `185 passed, 1 skipped` (KIS sandbox token rate-limit 1건만 skip; 그 외 PG/RabbitMQ 게이트 모두 통과).
- REQ-027 AC-1: `tests/adapters/portfolio/test_hub_read.py::test_get_portfolios_parses_envelope_and_filters_by_daemon` + `::test_get_portfolios_includes_items_without_daemon_field` + `test_stub_read.py::test_stub_returns_fixed_universe_without_network`.
- REQ-027 AC-2: `tests/adapters/portfolio/test_stub_read.py::test_stub_returns_fixed_universe_without_network` + `test_stub_items_are_mutable_for_tests`.
- REQ-027 AC-3: `tests/domain/test_domain_import_sweep.py::test_domain_has_no_forbidden_imports` (BAT-007 부터 유지, `httpx`/`pika`/`adapters.*` 금지 prefix 회귀 가드).
- REQ-027 AC-4: `tests/adapters/portfolio/test_hub_read.py::test_whoami_returns_token_daemon_id` (`/v1/user_info` URL/Bearer 헤더/응답 디코드 + daemon_id 추출) + `::test_base_url_without_scheme_defaults_to_http`.
- REQ-028 AC-1: `tests/application/test_snapshot_handler.py::test_envelope_shape_returned` + `tests/application/test_snapshot_selection.py::test_empty_envelope_assets_is_list_not_null`.
- REQ-028 AC-2: `tests/application/test_snapshot_handler.py::test_degraded_signal_does_not_leak_credentials` + `tests/application/test_snapshot_selection.py::test_degraded_signal_in_envelope`. 실 daemon 운영 검증에서 portfolio-hub 부재 환경에서 같은 응답 발생 확인.
- REQ-028 AC-3: `tests/application/test_snapshot_selection.py::test_envelope_shape_no_price_field`.
- REQ-029 AC-1: `tests/application/test_snapshot_selection.py::test_view_all_includes_holding_excludes_pending_for_same_symbol`.
- REQ-029 AC-2: `tests/application/test_snapshot_selection.py::test_view_buyable_with_zero_available_slots_returns_empty`.
- REQ-029 AC-3: `tests/application/test_snapshot_selection.py::test_view_held_returns_only_holding_slot_assets`.
- REQ-029 AC-4: `tests/application/test_snapshot_selection.py::{test_strategy_key_orthogonal_filter,test_strategy_key_missing_includes_everything_unmasked}`.
- REQ-029 AC-5: `tests/application/test_snapshot_selection.py::{test_warning_overlay_masks_asset_uniformly_across_views,test_pause_overlay_masks_asset}`.
- REQ-030 AC-1: `tests/application/test_snapshot_scheduler.py::test_run_once_fetches_universe_and_swaps_store` + `::test_swap_preserves_warning_for_carried_over_portfolio`. 실 daemon boot 후 첫 fetch 동작 확인.
- REQ-030 AC-2/AC-3: **deferred — portfolio-hub#8 배포 대기**. wiring point 테스트(`test_snapshot_scheduler.py::test_option_d_update_cash_position_overrides_baseline`) 가 `SnapshotStore.update_cash_position` 의 동작 보장.
- REQ-031 AC-1: `tests/application/test_reconciliation.py::test_mismatch_raises_warning_and_masks_asset`.
- REQ-031 AC-2: `tests/application/test_reconciliation.py::test_subsequent_match_auto_clears_warning`.
- REQ-031 AC-3: `tests/application/test_reconciliation.py::test_paper_broker_returns_deterministic_balance` (paper fill aggregate + 0수량 drop 시나리오).
- REQ-032 AC-1: `tests/application/test_pause_handler.py::test_pause_then_list` + `tests/application/test_snapshot_selection.py::test_pause_overlay_masks_asset`.
- REQ-032 AC-2: `tests/adapters/portfolio/test_postgresql_pause_store.py::test_durability_simulated_via_fresh_store_instance` (PG 게이트).
- REQ-032 AC-3: `tests/application/test_pause_handler.py::test_resume_clears_pause`.
- REQ-032 AC-4: `tests/application/test_pause_handler.py::test_cli_pause_visible_to_rpc_list_via_shared_store`. 실 daemon 운영 검증 — RPC pause → 같은 store list 노출 확인.
### Manual
- baseline ↔ 코드 정합 리뷰 (REQ-033 AC-1/AC-2):
- `docs/runtime-flows.md` 의 신규 5 섹션(`일일 스냅샷 refresh`, `포지션 정합 검증`, `pause/resume 제어`, `스냅샷 RPC 응답 산출`, `broker degrade 분기` 인접 위치) 가 실제 코드와 정합:
- `SnapshotScheduler.run_once()` 의 boot+tick 시퀀스 → `trex_core/application/snapshot_scheduler.py:run_forever`
- `reconcile_positions` symbol 집계 + warning set/clear → `trex_core/application/reconciliation.py`
- pause/resume CLI ↔ RPC ↔ `snapshot_pause` table → `trex_core/runtime/cli.py` + `trex_core/adapters/portfolio/pause_store.py` + `trex_core/application/messaging_handlers.py::make_pause_rpc_handler`
- envelope builder 의 base/mask/strategy_key 합성 → `trex_core/application/portfolio.py::build_snapshot_envelope`
- `docs/interface-contract.md` 의 `trex.snapshot.req` envelope 스키마 + `trex.pause.req` control RPC + `PortfolioHubReadPort` Protocol + `SnapshotStore`/`PauseStore`/`SnapshotScheduler` + `BrokerPort.get_balance()` 확장 섹션이 실제 시그니처와 정합.
- `docs/project-structure.md` 디렉토리 트리에서 `trex_core/adapters/portfolio/`, `trex_core/application/{portfolio,reconciliation,snapshot_scheduler}.py`, `trex_core/runtime/cli.py`, `003_create_snapshot_pause.sql`, daemon 의 6-스레드 구성, `trex-core` CLI console_script 모두 실제 트리(`ls -R trex_core/`)와 일치.
- code-search:
- `grep -r "from fastapi\|from uvicorn" trex_core/` → 0 (BAT-007 회귀 유지).
- `grep -r "import pika\|import httpx" trex_core/domain/` → 0 (NFR-1).
- `find trex_core/adapters/portfolio -type f -name '*.py'` → `__init__/hub_read/stub_read/pause_store.py` 4 파일 존재.
- `grep "PAUSE_REQ_QUEUE" trex_core/adapters/messaging/` → topology 상수 + rabbitmq declare 두 곳 노출.
### Logs
- `.venv/bin/ruff check trex_core tests` → `All checks passed!`.
- 실 RabbitMQ + PostgreSQL evidence: `127.0.0.1:5672` admin/admin + 로컬 PG 로 daemon 기동 후 snapshot RPC (view=all/buyable/held + strategy_key), pause/resume RPC, 응답 envelope 키/필드 검증 모두 통과. portfolio-hub 부재 환경에서 `degraded=true reason=portfolio_hub_unavailable` 정상 노출.
### Re-verification Addendum (2026-06-06, REQ-027 CHG-04 — release 선행 정리)
- 사유: release-candidate 확정 이후 실 portfolio-hub API 실측으로 read 계약 불일치 발견 → 어댑터/테스트 정정(commit 43bad24). 본 batch verification 을 정정 계약 기준으로 재확인.
- 정정 요지: whoami `/v1/auth/whoami`→`/v1/user_info`; list envelope `{items}`→`{portfolios,total}`; 토큰 스코프 → 클라이언트측 `managed_by_daemon_id` 필터; base URL 스킴 누락 시 `http://` 보정; 실 `PortfolioItem` 전략 필드 부재 → None/`{}`(이월).
- 재실행 결과:
- `.venv/bin/python -m pytest tests/adapters/portfolio/test_hub_read.py -q` → `6 passed`.
- `.venv/bin/python -m pytest -q` (infra env 미설정 invocation) → `165 passed, 23 skipped` (PG/RabbitMQ/KIS 게이트 skip). `ruff check trex_core tests` → `All checks passed!`.
- 실 portfolio-hub e2e (portfolio-hub#9 user_info 500 수정 반영 후): `PortfolioHubReadAdapter.whoami()`→`daemon_id=b5084542-…`, `get_portfolios()`→삼성전자(005930) 1건, `SnapshotScheduler.run_once()`→`degraded=None`, universe 1건, warning 없음.
- 영향 결론: 현재 코드가 실 계약과 일치하고 모든 AC evidence 가 정정 테스트명으로 재연결됨. REQ-027 Implemented 유지, BAT-008 release-candidate 유지. Ready for Release 변동 없음.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음.
## Human Approval Memo
- 승인자: Justin Ko (운영자 위임 — dcy-005 confirmed-by + REQ-030 Approved follow-up "portfolio-hub#8 배포 추적 후 옵션 D 구현 착수" 정책 적용)
- 승인 시각: 2026-06-06 (KST)
- 수용 범위 / 스킵 항목:
- **REQ-030 AC-2 / AC-3 (옵션 D — ledger write `*_after` 갱신)** : portfolio-hub#8 (`POST /v1/portfolios/{id}/ledger/trades` 응답 본문에 `cash_balance_after`/`asset_quantity_after`) 배포 전제. portfolio-hub 측 별도 배포(REL-003 등) 완료 전까지 trex-core 구현은 대기 — dcy-005 A-7 명시. 본 batch 의 wiring point(`SnapshotStore.update_cash_position`)는 이미 노출되어 있어 배포 후 LedgerWriter 의 ledger write 응답 소비 경로에서 호출 1줄 추가로 활성화 가능.
- 잔여 리스크:
- 옵션 D 활성 후 first ledger write 응답에서 실제 `*_after` 값을 사용해 idempotent 갱신이 동작하는지 통합 검증 필요(별도 후속 작업).
- KIS D-2 0수량 잔고 처리 규칙(설계 Decision 7)은 paper broker 회귀 가드만 있고 실 KIS 환경 관찰 evidence 부재 — 운영 단계에서 한 번 관찰 후 운영 노트화 권장.
- 스케줄러 daily fire 동작(boot 즉시 fire 외) 은 실 데몬 운영 후 첫 07:30 KST 도래 시 관찰 권장.
- 승인 근거:
- dcy-005 confirm-discovery 단계 `Handoff Decision: 승인` + REQ-030 confirm-req 단계 `Follow-up Action: portfolio-hub#8 배포 추적 후 옵션 D 구현 착수` 비차단 정책.
- 본 batch 의 M1-M8 / REQ-027~029, REQ-031~033 8개 acceptance criteria 모두 evidence 연결 완료.
- REQ-030 AC-1 (baseline 스케줄러)는 evidence 연결 완료. AC-2/AC-3 은 외부 의존(portfolio-hub#8) 대기 항목이며 본 batch wiring point 가 이미 준비됨.
- 후속 트래킹:
- portfolio-hub#8 배포 완료 알림 수신 시 별도 minor-change batch 로 옵션 D 구현 + REQ-030 재verify → REQ-030 status Approved → Implemented 전환.
@@ -1,28 +0,0 @@
# BAT-009 Implementation
- Status: completed
- Batch: BAT-009
## Plan Summary
- REQ-010 CHG-20260606-01 / AC-5: paper broker 튜닝 knob(REQ-011 AC-2 지연/부분/거부)을 데몬 env 로 노출. 미설정=기존 `PaperBroker()` 동작 byte-for-byte(회귀 없음).
## Changed Files
- trex_core/runtime/config.py: `PaperBrokerConfig` frozen dataclass(`fill_after_polls:int=0`, `qty_per_poll:Optional[int]=None`, `reject_symbols:frozenset[str]=frozenset()`). `DaemonConfig.paper` 필드(broker=='paper' 일 때만 참조). `from_env()` 에서 `TREX_PAPER_FILL_AFTER_POLLS`(int, 기본 0), `TREX_PAPER_QTY_PER_POLL`(빈 문자열→None), `TREX_PAPER_REJECT_SYMBOLS`(콤마 분리, 공백/빈 토큰 제거→frozenset) 파싱.
- trex_core/runtime/daemon.py: `_build_broker` paper 분기를 `PaperBroker(fill_after_polls=config.paper.fill_after_polls, qty_per_poll=config.paper.qty_per_poll, reject_symbols=set(config.paper.reject_symbols))` 로 교체.
- .env.example: `TREX_BROKER=paper` 아래 paper 튜닝 키 3개 주석 문서화(미설정=즉시 전량 체결·거부 없음).
- tests/runtime/test_config_dotenv.py: `_KEYS` 에 3개 키 추가, `test_paper_broker_defaults`(미설정=0/None/frozenset()), `test_paper_broker_env_tuning`(FILL_AFTER_POLLS=2, QTY_PER_POLL=4, REJECT_SYMBOLS="005930, 000660 ," → 2/4/{005930,000660}).
## Execution Log
1. `PaperBrokerConfig` dataclass + `DaemonConfig.paper` 필드 추가(기본값 = 기존 `PaperBroker()` 동작).
2. `from_env()` env 파싱(빈 문자열/공백 토큰 정규화).
3. `_build_broker` paper 분기 주입 + `.env.example` 문서화.
4. 단위테스트 2개 추가, 좁은 테스트 → 전체 회귀 → ruff 순서로 검증.
## Validation
- `pytest tests/runtime/test_config_dotenv.py`: **6 passed**(기존 4 + 신규 2).
- 전체 `pytest --deselect ...::test_sandbox_token_and_submit`(네트워크 의존 KIS sandbox 제외): **167 passed, 23 skipped**.
- `ruff check trex_core tests`: All checks passed.
- e2e(별도, verification.md 에 상세): 데몬 env 재기동으로 거부 롤백 `idle→pending_buy→idle`, 지연 체결 `+3.7s`, 부분 체결 qty 6 누적 `+2.5s` 재현.
## Remaining Risks
- 없음(저위험). broker=='paper' 일 때만 참조(kis 경로 무시), 미설정=기존 동작. 빈/공백 토큰 edge 는 단위테스트로 고정.
@@ -1,32 +0,0 @@
# BAT-009: paper broker 튜닝 env 노출 (minor-change)
- Status: released
- Profile: batch-lite
- Source Discovery: dcy-002 (REQ-010 데몬 설정 편의) — 동기는 dcy-003/REQ-011 e2e 시나리오 재현(Notes 참조)
- Included REQ: REQ-010 (CHG-20260606-01, AC-5)
- Excluded REQ: 없음
## Summary
- REQ-010 CHG-20260606-01 로 추가된 AC-5(paper broker 튜닝 env 노출)를 delivery 하는 minor-change batch-lite. REQ-011 AC-2 가 이미 요구하는 paper 더블의 결정적 시나리오(지연/부분/거부)를 데몬 재기동만으로 재현할 수 있도록, 기존 `PaperBroker` 생성자 knob 을 `.env`/OS env(`TREX_PAPER_FILL_AFTER_POLLS`/`TREX_PAPER_QTY_PER_POLL`/`TREX_PAPER_REJECT_SYMBOLS`)로 노출한다. 미설정이면 기존 `PaperBroker()` 동작과 byte-for-byte 동일(회귀 없음).
## Scope
- In Scope:
- `runtime/config.py`: `PaperBrokerConfig` frozen dataclass + `DaemonConfig.from_env()` 파싱(빈 문자열 → None/frozenset 정규화).
- `runtime/daemon.py`: `_build_broker` paper 분기에서 `config.paper` 주입.
- `.env.example`: paper 튜닝 키 3개 문서화(미설정=production-like 기본).
- `tests/runtime/test_config_dotenv.py`: 기본값(test_paper_broker_defaults) + env 파싱(test_paper_broker_env_tuning).
- Out of Scope:
- PaperBroker 내부 체결 로직 변경(REQ-011 에서 이미 구현·검증).
- kis broker 경로(`broker=kis` 시 paper config 무시).
- 부분 매도(수량 일부) 등 신규 능력(별도 Discovery/REQ).
## 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
- minor-change: 기존 PaperBroker 생성자 인자를 config 표면으로 노출하는 additive 변경. BrokerPort 계약·슬롯 런타임 흐름 불변, design 불요.
- 동기 출처: REQ-011(dcy-003) e2e 시나리오 재현 필요. REQ-010(dcy-002) 의 설정 로딩 메커니즘 위에 키를 얹는 형태라 REQ-010 change-req 로 귀속.
- 구현 코드는 본 batch 생성 시점에 이미 working tree 에 존재(미커밋). verification stage 에서 단위테스트(166 pass) + e2e evidence(거부 롤백/지연/부분)를 verification.md 에 정리한다.
@@ -1,32 +0,0 @@
# BAT-009 Planning
- Status: confirmed
- Batch: BAT-009
- Profile: batch-lite
## Included REQ
- REQ-010 (Configuration): 데몬 설정 .env 로딩 — CHG-20260606-01 / AC-5 (paper 튜닝 env 노출)
## Delivery Plan
1. `runtime/config.py`: `PaperBrokerConfig(fill_after_polls:int=0, qty_per_poll:Optional[int]=None, reject_symbols:frozenset[str]=frozenset())` 추가. `DaemonConfig` 에 `paper: PaperBrokerConfig` 필드(broker=='paper' 일 때만 참조). `from_env()` 에서 `TREX_PAPER_FILL_AFTER_POLLS`(int, 기본 0), `TREX_PAPER_QTY_PER_POLL`(빈 문자열→None), `TREX_PAPER_REJECT_SYMBOLS`(콤마 분리, 공백/빈 토큰 제거→frozenset) 파싱.
2. `runtime/daemon.py`: `_build_broker` paper 분기를 `PaperBroker(fill_after_polls=..., qty_per_poll=..., reject_symbols=set(...))` 로 교체.
3. `.env.example`: `TREX_BROKER=paper` 아래 paper 튜닝 키 3개 주석 문서화(미설정=즉시 전량 체결·거부 없음).
4. `tests/runtime/test_config_dotenv.py`: `_KEYS` 에 3개 키 추가, `test_paper_broker_defaults`(미설정=0/None/frozenset()), `test_paper_broker_env_tuning`(FILL_AFTER_POLLS=2, QTY_PER_POLL=4, REJECT_SYMBOLS="005930, 000660 ," → 2/4/{005930,000660}).
## Dependencies
- Internal: REQ-011 PaperBroker(생성자 knob 이미 존재), REQ-010 dotenv 로딩 메커니즘.
- External: 없음.
## Design Gate
- Design Required Before Implementation: no
- Reason: 기존 PaperBroker 생성자 인자를 config/env 표면으로 노출하는 additive wiring. 외부 인터페이스(BrokerPort)·슬롯 런타임 흐름 변경 없음. 미설정 시 기존 동작과 byte-for-byte 동일. batch-lite + design 생략(minor-change).
## Milestones
1. M1 — PaperBrokerConfig + from_env 파싱.
2. M2 — _build_broker 주입 + .env.example 문서화.
3. M3 — 단위테스트 green(기본/파싱) + ruff clean + 전체 회귀(166 pass).
4. M4 — e2e 시나리오 evidence(거부 롤백 `idle→pending_buy→idle`, 지연 `+3.7s`, 부분 qty 누적 `+2.5s`).
## Risks
- R-1 (Low): env 오설정이 production 데몬 동작을 바꿈. 대응: broker=='paper' 일 때만 참조(kis 경로 무시), 미설정=기본 동작, `.env.example` 에 "e2e 전용" 명시.
- R-2 (Low): 빈 문자열/공백 토큰 파싱 edge. 대응: `qty_per_poll` 빈 문자열→None, `reject_symbols` 빈/공백 토큰 제거 — 단위테스트로 고정.
@@ -1,27 +0,0 @@
# BAT-009 Verification
- Status: confirmed
- Batch: BAT-009
## Acceptance Mapping
- REQ-010 (CHG-20260606-01):
- AC1 `.env` 로드 → `test_loads_values_from_dotenv`. **PASS** (BAT-003 기존, 회귀 재확인)
- AC2 OS env 우선 → `test_os_env_takes_precedence_over_dotenv`. **PASS** (BAT-003 기존)
- AC3 `.env` 부재 회귀 → `test_no_dotenv_uses_os_env_only`. **PASS** (BAT-003 기존)
- AC4 `.gitignore` → `.env` 무시 / `.env.example` 추적. **PASS** (BAT-003 기존)
- **AC5 (본 batch)** paper 튜닝 env 파싱 + `_build_broker` 주입, 미설정=기본 동작 byte-for-byte:
- 단위: `test_paper_broker_defaults`(미설정 → fill_after_polls=0, qty_per_poll=None, reject_symbols=frozenset()). **PASS**
- 단위: `test_paper_broker_env_tuning`(FILL_AFTER_POLLS=2 / QTY_PER_POLL=4 / REJECT_SYMBOLS="005930, 000660 ," → 2 / 4 / {005930,000660}). **PASS**
- e2e(데몬 env 재기동): 거부(`TREX_PAPER_REJECT_SYMBOLS=999999`) → `idle→pending_buy→idle` 롤백; 지연(`TREX_PAPER_FILL_AFTER_POLLS=3`) → `pending_buy→holding +3.7s`; 부분(`TREX_PAPER_QTY_PER_POLL=2`, qty 6) → holding qty 6 누적 `+2.5s`. **PASS**
## Evidence
- Test: `pytest tests/runtime/test_config_dotenv.py` 6 passed(기존 4 + 신규 2). 전체 `pytest --deselect ...::test_sandbox_token_and_submit` **167 passed, 23 skipped**. `ruff check trex_core tests` All checks passed.
- e2e: 실 PostgreSQL + RabbitMQ + portfolio-hub(dev) 대상 데몬 e2e. 슬롯 전이 trail(`slot_transition_log`)·timestamp gap 으로 시나리오별 결정성 확인. (REL-001 Feedback Handoff Observation Summary 와 동일 evidence)
## no architecture impact confirmed
- batch-lite, planning Design Gate=no. 기존 `PaperBroker` 생성자 인자를 config/env 표면으로 노출하는 additive wiring 뿐 — BrokerPort 계약·슬롯 런타임 흐름 변경 없음. 미설정 시 기존 동작 byte-for-byte. baseline 문서(`docs/project-structure.md`/`docs/runtime-flows.md`) 영향 없음.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. e2e 는 paper broker 한정(운영 KIS 경로 무영향). REL-001 배포 scope 와 분리된 후속 release 로 배포.
@@ -1,50 +0,0 @@
# BAT-010 Design
- Status: confirmed
- Batch: BAT-010
## Architecture Summary
BAT-010은 safety-sell 기능의 기반 레이어 세 가지를 추가한다.
1. **WatchdogStore** (`application/watchdog.py` 신규): `dict[str, datetime]` in-memory store. snapshot RPC handler가 응답 슬롯의 `last_seen_at`을 갱신하고 BAT-011 트리거 루프가 만료 판정에 사용한다.
2. **도메인 필드 확장** (`models.py`, `postgresql.py`, SQL 마이그레이션): `stop_loss_price`, `take_profit_price` `Optional[Decimal]` additive 추가. BAT-011 KIS polling이 트리거 기준으로 사용한다.
3. **SlotService.sell()** (`domain/slot/service.py`): RabbitMQ 우회 내부 매도 경로. BAT-011의 모든 트리거 루프(forced_close, KIS polling, 장 마감)가 이 메서드를 호출한다.
## Changed Areas
- Docs:
- 없음 (baseline 문서 갱신은 REQ-042 / BAT-011에서 처리)
- Modules:
- `trex_core/runtime/config.py` — `DaemonConfig`에 safety-sell env vars 5종 파싱 추가
- `trex_core/domain/slot/models.py` — `stop_loss_price: Optional[Decimal]`, `take_profit_price: Optional[Decimal]` 추가
- `trex_core/adapters/persistence/postgresql.py` — 두 필드 read/write 포함
- `trex_core/adapters/persistence/sql/` — 마이그레이션 스크립트 신규 추가
- `trex_core/application/intents.py` — `IntentIn`에 `stop_loss_price`, `take_profit_price` Optional 필드 추가
- `trex_core/application/messaging_handlers.py` — `make_snapshot_rpc_handler` 확장(`last_seen_at` 갱신) + intent RPC payload 파싱 확장(price fields)
- `trex_core/application/watchdog.py` (신규) — `WatchdogStore` class
- `trex_core/domain/slot/service.py` — `SlotService.sell(slot_id, actor, reason)` 추가
- Interfaces:
- `SlotService.sell(slot_id: str, actor: str, reason: str) -> None` (신규 내부 인터페이스)
- `WatchdogStore.update(slot_id: str) -> None` (신규)
- `WatchdogStore.is_expired(slot_id: str, timeout_s: int) -> bool` (신규 — BAT-011 트리거 루프 계약)
- `WatchdogStore.in_grace_period(grace_s: int) -> bool` (신규)
## Key Decisions
1. **WatchdogStore를 `watchdog.py` 신규 모듈로 분리**: `portfolio.py`는 snapshot 조회·직렬화 책임이 있어 watchdog 관심사(타임스탬프 추적·만료 판정)를 추가하면 단일 책임 위반. 신규 모듈이 독립 테스트와 BAT-011 참조에 유리하다.
2. **REQ-041 env vars 5종 전체를 BAT-010에서 파싱 완성**: `TREX_FORCED_CLOSE_CHECK_INTERVAL`·`TREX_STOP_LOSS_POLL_INTERVAL`·`TREX_MARKET_CLOSE_LIQUIDATION_TIME`은 BAT-010에서 사용하지 않지만, REQ-041을 두 batch에 분리하면 REQ 단위 거버넌스 trace가 깨진다. 파싱만 완성하고 BAT-011이 config 값을 참조한다.
3. **WatchdogStore 동시성은 Python GIL에 의존**: `dict[str, datetime]` 단일 dict의 Python 레벨 read/write는 GIL 보장 하에 원자적. 별도 `threading.Lock` 불필요. grace period 판정: `datetime.utcnow() - self.started_at < timedelta(seconds=grace_s)` 단순 비교.
4. **SlotService.sell()은 transit_slot() 재사용**: 기존 `transit_slot(slot_id, from_state=HOLDING, to_state=PENDING_SELL, ...)` advisory lock + outbox 경로를 그대로 재사용. 신규 로직을 최소화해 BAT-004 검증 coverage를 그대로 이어받는다.
## Edge Cases
- grace period 중 `is_expired()` 호출: `in_grace_period()` True이면 store에 없는 슬롯 포함 전체 만료 판정 유예.
- 데몬 재시작 시 watchdog store: in-memory 초기화 + `started_at` 재설정으로 grace period 자동 재시작.
- `sell()` 호출 시 이미 `pending_sell` 상태: `transit_slot` 호출 전 상태 조회 → silent return (중복 매도 intent 제출 없음).
- `sell()` 호출 시 `idle`·`cancelled` 등 비정상 상태: `SlotStateError`(또는 동등 예외) raise, 슬롯 상태 불변.
- 마이그레이션 기존 row: `ALTER TABLE slots ADD COLUMN IF NOT EXISTS stop_loss_price NUMERIC` 패턴으로 멱등성 보장. 기존 row `NULL` 유지.
## Architecture Impact
- runtime-flows: snapshot RPC handler → watchdog `last_seen_at` 갱신 흐름 추가 (BAT-011 REQ-042에서 문서화)
- project-structure: `watchdog.py` 신규 모듈 추가 (BAT-011 REQ-042에서 문서화)
## Reference Doc Update Plan
- project-structure.md: BAT-011 REQ-042에서 처리 (`watchdog.py` 신규 모듈 반영)
- runtime-flows.md: BAT-011 REQ-042에서 처리 (snapshot RPC → watchdog update + sell() 내부 경로 반영)
@@ -1,43 +0,0 @@
# BAT-010 Implementation
- Status: completed
- Batch: BAT-010
## Plan Summary
- REQ-041 → REQ-035 → REQ-034 → REQ-036 순서로 구현. 각 단계 unit test green 확인 후 다음으로 진행.
## Changed Files
### 신규 파일
- `trex_core/application/watchdog.py` — WatchdogStore class (REQ-034)
- `trex_core/adapters/persistence/sql/004_add_price_thresholds.sql` — additive DB migration (REQ-035)
- `tests/runtime/test_safety_sell_config.py` — REQ-041 AC 검증 4종
- `tests/application/test_watchdog.py` — REQ-034 AC 검증 11종
- `tests/application/test_snapshot_rpc_watchdog.py` — REQ-034 handler 통합 4종
- `tests/domain/slot/test_service_sell.py` — REQ-036 AC 검증 6종
- `tests/domain/slot/test_price_threshold_fields.py` — REQ-035 AC 검증 7종
### 기존 파일 수정
- `trex_core/runtime/config.py` — SafetySellConfig dataclass 추가 + DaemonConfig 5종 env var 파싱 (REQ-041)
- `trex_core/domain/slot/models.py` — stop_loss_price / take_profit_price Optional[Decimal] 필드 추가 + EXPECTED_SLOT_FIELDS 갱신 (REQ-035)
- `trex_core/adapters/persistence/postgresql.py` — _SLOT_COLUMNS 갱신, update_slot SET절 확장, _row_to_slot 인덱스 조정 (REQ-035)
- `trex_core/application/intents.py` — BuyIntent에 price fields + Decimal import (REQ-035)
- `trex_core/domain/slot/service.py` — transit_slot에 stop_loss_price / take_profit_price 파라미터 추가, _apply_transition_fields 확장(reset + preserve), sell() 메서드 신설, Decimal import (REQ-035, REQ-036)
- `trex_core/application/messaging_handlers.py` — WatchdogStore import + SlotState import + make_snapshot_rpc_handler watchdog_store= 파라미터 + 홀딩 슬롯 update 로직 + buy intent payload _opt_decimal 파싱 (REQ-034, REQ-035)
## Execution Log
1. M1 — REQ-041: SafetySellConfig + DaemonConfig.from_env() 5종 파싱. test_safety_sell_config.py 4종 green.
2. M2 — REQ-035: models.py 필드 추가 + EXPECTED_SLOT_FIELDS 갱신 → test_contract.py 통과. postgresql.py _SLOT_COLUMNS / update_slot / _row_to_slot 갱신. SQL 마이그레이션 004 생성. intents.py BuyIntent 확장. service.py transit_slot + _apply_transition_fields 확장. test_price_threshold_fields.py 7종 green.
3. M3 — REQ-034: watchdog.py 신규. messaging_handlers.py make_snapshot_rpc_handler 확장. test_watchdog.py 11종 green. test_snapshot_rpc_watchdog.py 4종 green.
4. M4 — REQ-036: service.py sell() 신설. test_service_sell.py 6종 green.
5. M5 — 전체 회귀 95 passed (httpx 미설치 환경 기인 test_daemon_no_http_surface 1건 기존 실패, BAT-010 무관).
## Validation
- 신규 테스트: 32종 100% green
- 회귀 테스트: 95 passed (domain/ + runtime/ + application/ 대상, httpx 불필요 범위)
- test_contract.py: EXPECTED_SLOT_FIELDS ↔ dataclasses.fields(Slot) 계약 통과 (price fields 포함)
- `WatchdogStore.in_grace_period()` + `is_expired()` 조합 패턴 단위 테스트로 고정
## Remaining Risks
- R-2 (Medium → Low): DB 마이그레이션은 실 PostgreSQL 없이 검증 불가. 004 migration이 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` 패턴 사용 → 멱등 보장. 실 DB 검증은 BAT-011 환경 포함 후 수행.
- R-3 (Low): test_daemon_no_http_surface 기존 실패 — httpx 미설치 환경 이슈. BAT-010 변경 무관. 런타임 환경에서 httpx 설치 시 해소.
@@ -1,35 +0,0 @@
# BAT-010: safety-sell 기반 인프라 (watchdog + 가격 임계값 필드 + SlotService.sell)
- Status: released
- Profile: standard
- Source Discovery: dcy-007
- Included REQ: REQ-034, REQ-035, REQ-036, REQ-041
- Excluded REQ: REQ-037, REQ-038, REQ-039, REQ-040, REQ-042 (safety-sell 트리거 레이어 — BAT-011 예정)
## Summary
- dcy-007(core 자율 안전 매도) 기반 인프라 레이어를 delivery한다. per-slot 워치독 store(REQ-034), 슬롯 가격 임계값 도메인 필드 + DB 마이그레이션(REQ-035), 자율 매도 내부 경로 `SlotService.sell()`(REQ-036), safety-sell 환경 변수 전체 파싱(REQ-041)을 포함한다. 트리거 루프(forced_close/KIS polling/장 마감)와 `update_thresholds` RPC는 BAT-011에서 처리한다.
## Scope
- In Scope:
- `WatchdogStore` 신규 모듈(`application/watchdog.py`) + `make_snapshot_rpc_handler` 확장 (REQ-034)
- `stop_loss_price`, `take_profit_price` Optional[Decimal] 도메인 필드 + PostgreSQL additive 마이그레이션 (REQ-035)
- `SlotService.sell(slot_id, actor, reason)` + idempotency (REQ-036)
- `DaemonConfig` env vars 5종 파싱: `TREX_WATCHDOG_TIMEOUT`, `TREX_WATCHDOG_GRACE_PERIOD`, `TREX_FORCED_CLOSE_CHECK_INTERVAL`, `TREX_STOP_LOSS_POLL_INTERVAL`, `TREX_MARKET_CLOSE_LIQUIDATION_TIME` (REQ-041)
- Out of Scope:
- forced_close_at 루프 — BAT-011 (REQ-037)
- KIS 현재가 polling — BAT-011 (REQ-038)
- 장 마감 청산 루프 — BAT-011 (REQ-039)
- update_thresholds intent RPC — BAT-011 (REQ-040)
- baseline 문서 정렬 — BAT-011 (REQ-042)
- 가격 임계값 논리 유효성 검증(take_profit > stop_loss 등) — REQ-035 Notes 명시
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- BAT-010은 외부 API(KIS) 호출이 없고 신규 daemon thread를 추가하지 않는다. 단위 테스트와 persistence 통합 테스트만으로 모든 AC를 검증 가능한 레이어다.
- REQ-041의 env vars 5종 중 BAT-010에서 실제 사용되는 것은 `TREX_WATCHDOG_TIMEOUT` + `TREX_WATCHDOG_GRACE_PERIOD` 2개다. 나머지 3종(`TREX_FORCED_CLOSE_CHECK_INTERVAL`, `TREX_STOP_LOSS_POLL_INTERVAL`, `TREX_MARKET_CLOSE_LIQUIDATION_TIME`)은 파싱만 완성하고 BAT-011에서 참조한다. REQ-041을 두 batch에 분리하지 않기 위한 결정(design.md Key Decision 2 참조).
- WatchdogStore 구현 위치: `trex_core/application/watchdog.py` 신규 모듈(design.md Key Decision 1 참조).
@@ -1,42 +0,0 @@
# BAT-010 Planning
- Status: confirmed
- Batch: BAT-010
- Profile: standard
## Included REQ
- REQ-041 (Configuration): safety-sell 환경 변수 5종 파싱 — `TREX_WATCHDOG_TIMEOUT`(s, 기본 120), `TREX_WATCHDOG_GRACE_PERIOD`(s, 기본 60), `TREX_FORCED_CLOSE_CHECK_INTERVAL`(s, 기본 60), `TREX_STOP_LOSS_POLL_INTERVAL`(s, 기본 30), `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST, optional)
- REQ-035 (Data): `stop_loss_price` · `take_profit_price` `Optional[Decimal]` 도메인 필드 + PostgreSQL additive 마이그레이션
- REQ-034 (Interface): per-slot 워치독 — `WatchdogStore(dict[slot_id, datetime])` + snapshot RPC handler 확장 + grace period
- REQ-036 (Interface): `SlotService.sell(slot_id, actor, reason)` — `holding→pending_sell` 전이 + idempotency
## Delivery Plan
1. REQ-041: `runtime/config.py`에 safety-sell env vars 5종 파싱 추가 (`DaemonConfig` 확장). 파싱 단위 테스트.
2. REQ-035: `domain/slot/models.py`에 두 필드 추가 → `adapters/persistence/postgresql.py` read/write 포함 → SQL 마이그레이션 스크립트 작성 → `application/intents.py` + `messaging_handlers.py` payload 파싱 추가. 필드 저장·조회 round-trip 테스트 + 마이그레이션 멱등성 테스트.
3. REQ-034: `application/watchdog.py` 신규 작성 (`WatchdogStore` class, grace period 판정) → `make_snapshot_rpc_handler` 확장 (응답 슬롯 `last_seen_at` 갱신). 워치독 판정 단위 테스트 (만료/비만료/grace period 시나리오).
4. REQ-036: `domain/slot/service.py`에 `sell(slot_id, actor, reason)` 추가 (`transit_slot` 기반, idempotency). 단위 테스트 3종(holding→pending_sell, pending_sell 중복 호출, idle 오류).
5. 전체 회귀 테스트 green + ruff lint clean.
## Dependencies
- Internal:
- BAT-007/REQ-022: `make_snapshot_rpc_handler` 기존 구현 — 확장 대상
- BAT-008/REQ-027: `SlotService` 기존 구조 + `transit_slot` 패턴 — `sell()` 기반
- BAT-004/REQ-011~012: `PaperBroker` execution 경로 — `sell()` 통합 검증 대상
- External:
- 없음 (KIS API 호출 없음, 신규 daemon thread 없음)
## Design Gate
- Design Required Before Implementation: yes
- Reason: `WatchdogStore`는 신규 in-memory 컴포넌트로 snapshot RPC handler thread에서 write, BAT-011 트리거 루프 thread에서 read가 발생한다. 동시성 모델(GIL 의존 vs. explicit lock)과 모듈 배치(`watchdog.py` 분리 vs. `portfolio.py` 확장)를 설계 단계에서 확정해야 BAT-011 트리거 루프 설계 시 인터페이스 충돌이 없다. `SlotService.sell()`과 `transit_slot` advisory lock 통합 방식도 설계에서 명시한다.
## Milestones
1. M1 — REQ-041: env vars 파싱 + unit test green
2. M2 — REQ-035: 도메인 필드 + 마이그레이션 + persistence round-trip 테스트 green
3. M3 — REQ-034: `WatchdogStore` + snapshot RPC handler 확장 + 워치독 판정 unit test green
4. M4 — REQ-036: `SlotService.sell()` + idempotency unit test green (3종)
5. M5 — 전체 회귀 + ruff lint clean
## Risks
- R-1 (Medium): WatchdogStore 동시성 — BAT-011 트리거 루프가 watchdog store를 read하는 시점에 snapshot RPC handler가 동시 write할 수 있음. 대응: Python GIL 보장 하에 단순 `dict` 사용; 별도 `threading.Lock` 회피. 단위 테스트에서 판정 로직 고정.
- R-2 (Medium): DB 마이그레이션 기존 row 영향. 대응: `NULL` 허용 additive 컬럼 추가 전용(`ALTER TABLE … ADD COLUMN IF NOT EXISTS`). 기존 row `NULL` 유지. migration 멱등성 테스트.
- R-3 (Low): `SlotService.sell()` advisory lock 통합 — `transit_slot` 기존 advisory lock과 충돌 가능성. 대응: 기존 `transit_slot()` 코드 경로를 최대한 재사용하고 신규 로직을 최소화한다.
@@ -1,78 +0,0 @@
# BAT-010 Verification
- Status: verified
- Batch: BAT-010
## Acceptance Mapping
### REQ-041 (SafetySellConfig env vars)
| AC | Evidence |
|----|---------|
| AC-1: 기본값 120/60/60/30/None | `test_safety_sell_config.py::test_safety_sell_config_defaults` — 5개 기본값 모두 assert |
| AC-2: TREX_WATCHDOG_TIMEOUT 파싱 | `test_safety_sell_config_env_tuning` — 180 파싱 확인 |
| AC-3: TREX_WATCHDOG_GRACE_PERIOD 파싱 | 동일 테스트 — 90 파싱 확인 |
| AC-4: TREX_FORCED_CLOSE_CHECK_INTERVAL 파싱 | 동일 테스트 — 45 파싱 확인 |
| AC-5: TREX_STOP_LOSS_POLL_INTERVAL 파싱 | 동일 테스트 — 15 파싱 확인 |
| TREX_MARKET_CLOSE_LIQUIDATION_TIME Optional | `test_safety_sell_config_env_tuning` "15:30" 파싱 + `test_market_close_liquidation_time_empty_string_becomes_none` None 변환 |
### REQ-035 (stop_loss_price / take_profit_price 필드)
| AC | Evidence |
|----|---------|
| AC-1: 기본값 None | `test_price_threshold_fields.py::test_price_fields_default_to_none` |
| AC-2: Decimal 저장·조회 round-trip | `test_price_fields_set_on_pending_buy` — Decimal("48000") round-trip via in-memory repo |
| AC-3: 마이그레이션 멱등성 | `004_add_price_thresholds.sql` `IF NOT EXISTS` 패턴. 실 PostgreSQL round-trip은 Remaining Risks R-2 참조 |
| AC-4: 매수 intent payload 포함 시 저장 | `test_price_fields_preserved_through_holding` — pending_buy→holding 전이 후 price fields 유지 |
| AC-5: 미포함 시 None | `test_no_price_fields_sets_none` |
| 계약 가드 | `test_contract.py::test_slot_fields_match_expected_contract` — EXPECTED_SLOT_FIELDS 포함 확인 |
### REQ-034 (per-slot WatchdogStore)
| AC | Evidence |
|----|---------|
| AC-1: snapshot RPC 처리 시 holding slot last_seen_at 갱신 | `test_snapshot_rpc_watchdog.py::test_holding_slot_watchdog_updated_on_snapshot_rpc` |
| AC-2: timeout 초과 슬롯 만료 판정 | `test_watchdog.py::TestIsExpired::test_old_update_is_expired` (200s > 120s timeout) |
| AC-3: timeout 미초과 슬롯 비만료 | `test_just_within_timeout_not_expired` (100s < 120s timeout) |
| AC-4: grace period 중 만료 유예 | `test_grace_period_defers_expiry_check` — in_grace_period+is_expired 조합 패턴 검증 |
| AC-5: 재시작 시 store 초기화 | `test_new_store_is_empty` |
| AC-6: 정상 poll 주기 슬롯 비만료 | `test_recently_updated_slot_not_expired` — update 직후 is_expired=False |
| holding만 갱신, idle은 제외 | `test_idle_slot_watchdog_not_updated` |
| 복수 holding 슬롯 모두 갱신 | `test_multiple_holding_slots_all_updated` |
| backward compat (watchdog_store=None) | `test_no_watchdog_store_does_not_raise` |
### REQ-036 (SlotService.sell() + idempotency)
| AC | Evidence |
|----|---------|
| AC-1: holding → pending_sell 전이 | `test_service_sell.py::test_sell_holding_transitions_to_pending_sell` |
| AC-2: pending_sell 중복 호출 → 무전이 | `test_sell_idempotent_on_pending_sell` — transition_log PENDING_SELL 항목 1건만 확인 |
| AC-3: idle 호출 → 오류, 상태 불변 | `test_sell_idle_slot_raises_transition_error` |
| AC-4: execution 경로 재사용 | `test_sell_records_transition_log` — transit_slot 기반으로 log append 확인 |
| AC-5: transport 독립 | `sell()` 구현이 RabbitMQ 의존 없는 service.transit_slot() 직접 호출 — 코드 검사로 확인 |
| SlotNotFoundError | `test_sell_unknown_slot_raises_not_found` |
| price fields 보존 | `test_sell_preserves_stop_loss_price` |
## Evidence
- Test:
- 신규 테스트 32종 all green (pytest 출력 확인)
- `tests/domain/ + tests/runtime/ + 대상 tests/application/` 회귀 95 passed
- `test_contract.py` 통과 — EXPECTED_SLOT_FIELDS 계약 유지
- Manual:
- 없음 (단위 테스트로 모든 AC 커버)
- Logs:
- `32 passed in 0.09s` (신규 테스트 단독)
- `95 passed in 0.13s` (회귀 포함)
## Architecture Impact Note
- BAT-010은 project-structure.md / runtime-flows.md 업데이트를 BAT-011 REQ-042에 위임
- design.md Reference Doc Update Plan과 일치: 현 batch는 구현 evidence 확보 범위
- `test_daemon_no_http_surface` 기존 실패는 httpx 미설치 환경 이슈로 BAT-010 변경과 무관
## Result
- Ready for Release: true
- Blocking Issues:
- R-2: 실 PostgreSQL migration round-trip 미검증 — `IF NOT EXISTS` 패턴으로 안전성 확보, BAT-011 환경에서 검증 예정. release gate에서는 수용(additive-only schema 변경, 기존 row 영향 없음).
@@ -1,55 +0,0 @@
# BAT-011 Design
- Status: confirmed
- Batch: BAT-011
## Architecture Summary
BAT-011은 BAT-010 기반 위에 자동 매도를 실제로 발동하는 트리거 레이어를 추가한다.
1. **`trex-safety-sell` worker 스레드** (`runtime/daemon.py`): 기존 6 스레드(execution/ledger/intent-rpc/snapshot-rpc/pause-rpc/snapshot-scheduler)에 7번째로 추가. `_safety_sell_loop()`가 단일 통합 루프에서 forced_close_at 만기(REQ-037), stop-loss·take-profit 가격 체크(REQ-038), 장 마감 청산(REQ-039)을 순차 평가한다.
2. **KIS 현재가 polling 어댑터** (`adapters/broker/kis.py` 확장): `get_current_price(symbol) -> Decimal` — `TTTC8397R`(실전)/`VTTC8397R`(모의) 시세 endpoint 호출, `stck_prpr`(직전 체결가) 파싱. paper 환경 stub.
3. **`update_thresholds` intent RPC** (`application/messaging_handlers.py` + `domain/slot/service.py`): `make_intent_rpc_handler`에 action 라우팅 추가, `SlotService.update_thresholds()`로 부분 갱신 + 워치독 `last_seen_at` 리셋.
4. **baseline 문서 정합** (REQ-042): 구현 완료 후 3문서 갱신.
## Changed Areas
- Docs:
- `docs/interface-contract.md` — `update_thresholds` action 계약 + intent payload `stop_loss_price`/`take_profit_price` 선택 필드 + snapshot RPC 워치독 갱신 신호 명시 (REQ-040, REQ-042)
- `docs/runtime-flows.md` — 워치독 갱신·forced_close 체크·가격 체크·장 마감 청산 흐름 추가 + `:114` "A-5 미구현" 주석 갱신 (REQ-042)
- `docs/project-structure.md` — `trex-safety-sell` 스레드 + env 5종 + KIS 현재가 polling/watchdog 모듈 반영 (REQ-042)
- Modules:
- `trex_core/runtime/daemon.py` — `trex-safety-sell` worker 등록 + `_safety_sell_loop()` 구현(forced_close/가격/장 마감 통합)
- `trex_core/adapters/broker/kis.py` — `get_current_price(symbol)` 추가(`TTTC8397R`/`VTTC8397R`, `stck_prpr`)
- `trex_core/application/broker_port.py` — (필요 시) `get_current_price` 포트 시그니처 추가; paper 더블 stub 구현
- `trex_core/adapters/broker/paper.py` — `get_current_price` stub(고정/주입 가능 현재가) — 테스트·모의용
- `trex_core/domain/slot/service.py` — `SlotService.update_thresholds(slot_id, stop_loss_price, take_profit_price, forced_close_at)` 추가 + 트리거 조건 평가 헬퍼(또는 worker 인라인)
- `trex_core/application/messaging_handlers.py` — `make_intent_rpc_handler`에 `action: "update_thresholds"` 라우팅 + holding-only 응답
- Interfaces:
- `Broker.get_current_price(symbol: str) -> Decimal | None` (신규 포트 메서드)
- `SlotService.update_thresholds(slot_id, stop_loss_price=..., take_profit_price=..., forced_close_at=...) -> str` (부분 갱신, sentinel로 "미지정" vs "null 해제" 구분)
- intent RPC `action: "update_thresholds"` payload/response 계약 (REQ-040)
## Key Decisions (초안 — 구현 전 확정 대상)
1. **단일 통합 worker 루프 vs. 분리 스레드**: REQ-037 Notes 권고에 따라 `trex-safety-sell` 단일 스레드 통합 루프를 기본안으로 한다. 가격 polling 주기(`TREX_STOP_LOSS_POLL_INTERVAL`, 기본 30s)와 forced_close 주기(`TREX_FORCED_CLOSE_CHECK_INTERVAL`, 기본 60s)가 다르므로, 루프 base tick을 짧은 쪽(또는 GCD)으로 두고 각 체크에 last-run 타임스탬프로 sub-scheduling한다. (확정 필요)
2. **KIS 현재가 어댑터 배치**: `kis.py`에 `get_current_price` 추가(별도 `kis_price.py` 분리 대신) — 토큰/세션/`_account` 재사용 이점. paper는 `paper.py` stub. (확정 필요)
3. **WatchdogStore read 동시성**: BAT-010 Key Decision 3 계약 승계 — GIL 보장 단일 dict, lock 없음. worker는 `is_expired()`/`in_grace_period()` read만 수행.
4. **장 마감 1회 발동 가드**: in-memory `last_liquidation_date: date | None`. 재시작 시 초기화(REQ-039 Notes). 같은 거래일 재발동 방지. 발동 시각 도래 판정은 `now(KST).time() >= liquidation_time and last_liquidation_date != today`.
5. **`update_thresholds` 부분 갱신 sentinel**: payload에 키가 없으면 "유지", 키가 `null`이면 "해제(NULL)". `_UNSET` sentinel로 두 경우를 구분(REQ-040 AC-4/AC-5).
6. **루프 예외 격리**: tick 단위 try/except로 polling/sell 예외가 worker 스레드를 죽이지 않게 한다. 예외는 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).
## Edge Cases
- 가격 polling 중 KIS 거부/타임아웃: 해당 슬롯 skip + 경고 로그, 다음 tick 재시도(R-4).
- forced_close와 stop-loss가 동시 충족: 둘 다 `sell()` 호출하나 REQ-036 idempotency로 1회만 전이.
- 장 마감 청산 시각이 forced_close 체크 사이에 도래: 통합 루프 tick에서 장 마감 평가가 우선 실행되도록 순서 고정.
- `update_thresholds`로 `forced_close_at`만 갱신: 나머지 필드 유지(sentinel), `last_seen_at` 리셋(워치독 타이머 재시작, REQ-034).
- non-holding 슬롯에 `update_thresholds`: 필드 불변 + `{status: ignored, reason: "not_holding"}` (REQ-040 AC-6).
- 쿼터 상한 초과: 경고 로그만(본 batch 범위) — 동적 간격 조정은 out of scope.
## Architecture Impact
- runtime-flows: safety-sell worker 4종 흐름(워치독 갱신은 snapshot RPC 측) 추가 — REQ-042에서 문서화.
- project-structure: 7번째 스레드 `trex-safety-sell` + KIS 현재가 polling 어댑터 메서드 — REQ-042에서 문서화.
- broker_port 계약 확장(`get_current_price`)이 paper/kis 양 더블에 반영되어야 한다.
## Reference Doc Update Plan
- `interface-contract.md`: `update_thresholds` action + price 선택 필드 + snapshot RPC 워치독 신호 (REQ-042 AC-1/AC-2)
- `runtime-flows.md`: 4종 safety-sell 흐름 + `:114` A-5 주석 갱신 (REQ-042 AC-3/AC-4)
- `project-structure.md`: `trex-safety-sell` 스레드 + env 5종 + 신규 모듈 (REQ-042 AC-5)
@@ -1,44 +0,0 @@
# BAT-011 Implementation
- Status: completed
- Batch: BAT-011
## Plan Summary
- 구현 순서: REQ-038 KIS 현재가 어댑터 → `SafetySellService`(REQ-037/038/039) + `trex-safety-sell` worker 배선 → REQ-040 `update_thresholds` RPC → REQ-042 baseline 문서 정합. 각 단계 unit test green 확인 후 진행.
## Changed Files
### 신규 파일
- `trex_core/application/safety_sell.py` — `SafetySellService`: forced_close(REQ-037) / stop-loss·take-profit(REQ-038) / 장 마감 청산(REQ-039) + run_once 캐던스. watchdog read-only, sell() 통해 매도, tick 예외 격리.
- `tests/adapters/broker/test_kis_price.py` — KIS 현재가 어댑터 7종(tr_id 분기, stck_prpr 파싱, 에러→None, paper stub).
- `tests/application/test_safety_sell_worker.py` — 트리거 서비스 23종(forced_close 6 / 가격 10 / 장 마감 6 / run_once 2).
- `tests/application/test_update_thresholds_handler.py` — update_thresholds RPC 8종(부분 갱신/null 해제/non-holding/last_seen 리셋/미존재).
### 기존 파일 수정
- `trex_core/application/broker_port.py` — `BrokerPort.get_current_price(symbol) -> Optional[Decimal]` 포트 추가 (REQ-038).
- `trex_core/adapters/broker/kis.py` — `_tr_price()`(TTTC8397R/VTTC8397R) + `get_current_price()`(`inquire-price`, `stck_prpr` 파싱, 에러→None) (REQ-038).
- `trex_core/adapters/broker/paper.py` — `set_current_price()`/`get_current_price()` 프로그래머블 stub (REQ-038).
- `trex_core/domain/slot/service.py` — `SlotService.update_thresholds()` + `_UNSET` sentinel(유지 vs 해제 구분) (REQ-040).
- `trex_core/application/messaging_handlers.py` — `make_intent_rpc_handler` 에 `slot_service`/`watchdog_store` 인자 + `update_thresholds` action 라우팅(`_handle_update_thresholds`) + `last_seen_at` 리셋 (REQ-040).
- `trex_core/runtime/daemon.py` — `WatchdogStore` 인스턴스 + `SafetySellService` 빌드 + `trex-safety-sell` worker(7번째 스레드) + snapshot/intent RPC handler 에 watchdog 배선 + docstring 갱신 (REQ-034/037/038/039/040).
- `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/project-structure.md` — baseline 정합 (REQ-042).
- `tests/domain/slot/test_price_threshold_fields.py` — 중복 import 정리(ruff clean).
## Execution Log
1. broker_port + kis + paper 현재가 구현 → `test_kis_price.py` 7 passed.
2. `safety_sell.py` 구현 → `test_safety_sell_worker.py` 23 passed.
3. daemon 배선(watchdog + worker + handler) → import 회귀(test_daemon_no_http_surface) green.
4. `update_thresholds` 구현 → `test_update_thresholds_handler.py` 8 passed.
5. baseline 3문서 정합(REQ-042).
6. 전체 회귀: 단위 236 passed / 23 skipped, `ruff check` All passed.
7. 인프라 게이트 포함(PG+RabbitMQ): 259 passed (마이그레이션 001~004 적용 + PG repo round-trip 포함).
8. **배포 전 실 KIS LIVE 시세 검증(읽기 전용)** → REQ-038 시세 tr_id 결함 발견(`TTTC8397R` = "없는 서비스 코드", rt_cd=1). change-req REQ-038 CHG-04/05 로 `FHKST01010100`(실전/모의 공용) 정정 → `kis.py _tr_price` 수정 + `test_kis_price` AC-7 정정 → LIVE 재검증 `get_current_price('005930')=329000`(rt_cd=0). 단위 236 passed 재확인.
## Validation
- 단위: `pytest -q --deselect ...::test_sandbox_token_and_submit` → 236 passed / 23 skipped.
- 인프라 게이트: `TREX_RABBITMQ_TEST_URL=... TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) pytest -q` → 259 passed.
- lint: `ruff check trex_core tests` → All checks passed.
## Remaining Risks
- R-1 (운영 실증 이월): stop-loss·take-profit·forced_close·장 마감의 **실 KIS 라이브 트리거**는 휴장/운영 환경 제약으로 단위(mock)·정적 검증까지 수행. 차기 거래일 운영 관찰 필요(verification Carried-Over).
- R-5 해소: BAT-010 R-2(실 PostgreSQL `004_add_price_thresholds.sql` round-trip)는 인프라 게이트 259 passed 로 적용 확인(마이그레이션 idempotency + PG repo 테스트 green).
@@ -1,35 +0,0 @@
# BAT-011: safety-sell 트리거 레이어 (forced_close·KIS polling·장 마감 청산 + update_thresholds RPC + baseline 정합)
- Status: released
- Profile: standard
- Source Discovery: dcy-007
- Included REQ: REQ-037, REQ-038, REQ-039, REQ-040, REQ-042
- Excluded REQ: REQ-034, REQ-035, REQ-036, REQ-041 (safety-sell 기반 인프라 — BAT-010에서 delivery 완료)
## Summary
- dcy-007(core 자율 안전 매도)의 트리거 레이어를 delivery한다. BAT-010이 제공한 기반(WatchdogStore, `stop_loss_price`/`take_profit_price` 도메인 필드, `SlotService.sell()`, safety-sell env vars 5종 파싱) 위에 실제 자동 매도를 발동하는 백그라운드 루프와 임계값 갱신 RPC를 추가한다. 구성: `forced_close_at` 만기 루프(REQ-037), stop-loss·take-profit KIS 현재가 polling + 트리거(REQ-038), 장 마감 강제 청산(REQ-039), `update_thresholds` intent RPC 액션(REQ-040), 그리고 dcy-007 전체(REQ-034~041)의 baseline 문서 정합(REQ-042).
## Scope
- In Scope:
- 신규 `trex-safety-sell` 데몬 worker 스레드 — 기존 6 스레드에 1개 추가. forced_close_at 체크(REQ-037), stop-loss·take-profit 가격 체크(REQ-038), 장 마감 청산(REQ-039)을 통합 루프로 처리.
- forced_close_at 만기 감지: `holding` + 워치독 만료 + `forced_close_at <= now(KST)` → `SlotService.sell()` (REQ-037)
- KIS 현재가 polling 어댑터(`TTTC8397R` 실전 / `VTTC8397R` 모의, `stck_prpr` 파싱) + 손절/익절 조건 평가 + 쿼터 상한 경고 (REQ-038)
- 장 마감 강제 청산: `TREX_MARKET_CLOSE_LIQUIDATION_TIME` 도래 시 워치독 무관 전체 `holding` 청산 + 거래일 1회 발동 가드 (REQ-039)
- `update_thresholds` intent RPC 액션: `stop_loss_price`/`take_profit_price`/`forced_close_at` 부분 갱신 + 워치독 `last_seen_at` 리셋 + holding-only (REQ-040)
- baseline 3문서(`interface-contract.md`, `runtime-flows.md`, `project-structure.md`) 정합 — dcy-007 REQ-034~041 반영 (REQ-042)
- Out of Scope:
- BAT-010이 완료한 기반 인프라(REQ-034 WatchdogStore, REQ-035 도메인 필드/마이그레이션, REQ-036 `SlotService.sell()`, REQ-041 env 파싱) 재구현 — 본 batch는 호출/참조만 한다.
- 가격 임계값 논리 유효성 검증(take_profit > stop_loss 등) — dcy-007/REQ-035 범위 밖.
- KIS 쿼터 초과 시 polling 간격 자동 조정의 동적 튜닝 정책 — 본 batch는 상한 산정 + 경고 로그까지(REQ-038 AC-8). 동적 조정은 후속 후보.
## 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-007). BAT-010과 같은 Discovery의 후속 batch이며, BAT-010 index의 "BAT-011 예정" 분할 계획과 정확히 일치한다(REQ-037/038/039/040/042).
- 본 batch는 BAT-010과 달리 **외부 API(KIS 현재가 polling) 호출과 신규 daemon thread**를 도입하므로 standard profile + design 선행이 필요하다(planning.md Design Gate 참조).
- BAT-010 verification의 잔여 R-2(실 PostgreSQL migration `004_add_price_thresholds.sql` round-trip 미검증)를 본 batch 환경에서 함께 검증한다(verification에 evidence 기록 예정).
- REQ-042(문서 정합)는 REQ-037~040 구현 완료 후 verification 단계에서 최신 구현 상태를 반영해 수행한다(REQ-042 Notes).
@@ -1,54 +0,0 @@
# BAT-011 Planning
- Status: confirmed
- Batch: BAT-011
- Profile: standard
## Included REQ
- REQ-037 (Integration): `forced_close_at` 만기 자동 매도 루프 — `holding` + 워치독 만료 + `forced_close_at <= now(KST)` 슬롯에 `SlotService.sell()`. `TREX_FORCED_CLOSE_CHECK_INTERVAL` 주기. KIS 호출 없음(DB + in-memory watchdog 비교).
- REQ-038 (Integration): stop-loss·take-profit KIS 현재가 polling + 트리거 — `TTTC8397R`(실전)/`VTTC8397R`(모의) `stck_prpr` 기준, `현재가 <= stop_loss_price`(손절)/`현재가 >= take_profit_price`(익절) 시 `sell()`. `TREX_STOP_LOSS_POLL_INTERVAL` 주기. 쿼터 상한 `N × (3600/interval)` 경고.
- REQ-039 (Integration): 장 마감 강제 청산 — `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST) 도래 시 워치독 무관 전체 `holding` 청산. 거래일 1회 발동 가드(`last_liquidation_date`).
- REQ-040 (Interface): `update_thresholds` intent RPC 액션 — `{action, slot_id, stop_loss_price?, take_profit_price?, forced_close_at?}` 부분 갱신 + 워치독 `last_seen_at` 리셋 + holding-only. `reply_to` 시 `{status: updated|ignored, slot_id}` 응답.
- REQ-042 (Documentation): baseline 3문서 정합 — `interface-contract.md`(update_thresholds + price 필드), `runtime-flows.md`(워치독 갱신·forced_close·가격 체크·장 마감 흐름 + `:114` A-5 주석 갱신), `project-structure.md`(`trex-safety-sell` 스레드 + env 5종 + KIS polling/watchdog 모듈).
## Delivery Plan
1. REQ-038 KIS 현재가 polling 어댑터: `get_current_price(symbol)` (`TTTC8397R`/`VTTC8397R`, `stck_prpr` 파싱, 실전/모의 분기). paper 환경 stub. 어댑터 단위 테스트.
2. REQ-037 + REQ-038 + REQ-039 통합 worker: `daemon.py`에 `trex-safety-sell` 스레드 + `_safety_sell_loop()` 추가. 루프 1 tick에서 (a) forced_close_at 만기, (b) stop-loss/take-profit 가격 체크, (c) 장 마감 청산 시각 평가를 순차 수행. 각 경로 `SlotService.sell()` 호출. mock 기반 단위 테스트(만기/손절/익절/미충족/워치독 보호/idempotency/장 마감 전체·중복가드).
3. REQ-040 `update_thresholds`: `SlotService.update_thresholds(...)` + `make_intent_rpc_handler` action 라우팅 + `last_seen_at` 리셋 + holding-only 응답. handler 단위 테스트(부분 갱신/null 해제/non-holding ignored/last_seen_at 리셋).
4. REQ-039 config: `TREX_MARKET_CLOSE_LIQUIDATION_TIME` 활용 확인(REQ-041에서 파싱 완료 — 본 batch는 소비).
5. REQ-042 baseline 문서 3종 정합 — 구현 완료 상태 반영.
6. 전체 회귀 green + ruff lint clean + BAT-010 잔여 R-2(실 PG 마이그레이션 round-trip) 검증.
## Dependencies
- Internal:
- BAT-010/REQ-034: `WatchdogStore.is_expired()` / `in_grace_period()` — 트리거 루프의 워치독 만료 판정 계약(read 측).
- BAT-010/REQ-035: `stop_loss_price`/`take_profit_price` 도메인 필드 — 가격 체크 기준값.
- BAT-010/REQ-036: `SlotService.sell(slot_id, actor, reason)` — 모든 트리거의 매도 제출 경로.
- BAT-010/REQ-041: safety-sell env vars 5종 파싱(`DaemonConfig`) — 본 batch가 값을 소비.
- BAT-007/REQ-022: `make_intent_rpc_handler` — `update_thresholds` action 확장 대상.
- BAT-004/REQ-012: `KISBroker`(`kis.py`) — 현재가 polling 어댑터 추가/확장 대상.
- External:
- KIS Open API 현재가 시세 endpoint(`TTTC8397R`/`VTTC8397R`). 실 호출은 운영(`broker=kis`) 환경; 테스트는 mock/stub.
## Design Gate
- Design Required Before Implementation: yes
- Reason: 본 batch는 (a) **신규 daemon worker 스레드(`trex-safety-sell`)**를 추가하고 BAT-010의 WatchdogStore를 새로운 스레드에서 read하므로 동시성 계약(GIL 의존 read vs. snapshot RPC handler write)을 명시해야 한다. (b) **신규 외부 I/O(KIS 현재가 polling)** 어댑터의 배치(`kis.py` 확장 vs. 신규 `kis_price.py`), 실전/모의 endpoint 분기, paper stub 전략, 쿼터 상한 산정을 설계에서 확정해야 한다. (c) forced_close·가격 체크·장 마감을 **단일 통합 루프**로 묶는 구조와 장 마감 1회 발동 가드(`last_liquidation_date`) 상태 모델을 설계에서 고정해야 구현·검증 drift가 없다. → `design.md`를 동일 턴에 scaffold한다.
## Milestones
1. M1 — REQ-038 KIS 현재가 polling 어댑터 + 단위 테스트 green (실전/모의 분기, `stck_prpr` 파싱)
2. M2 — `trex-safety-sell` worker + forced_close_at 만기 루프(REQ-037) + 단위 테스트 green
3. M3 — stop-loss·take-profit 가격 체크(REQ-038) worker 통합 + mock 단위 테스트 green(손절/익절/미충족/워치독 보호/쿼터 산정)
4. M4 — 장 마감 강제 청산(REQ-039) + 거래일 1회 가드 + 단위 테스트 green
5. M5 — `update_thresholds` RPC(REQ-040) + handler 단위 테스트 green
6. M6 — baseline 3문서 정합(REQ-042) + 전체 회귀 + ruff clean + BAT-010 R-2 검증
## Risks
- R-1 (Medium): KIS 현재가 polling 쿼터 — 워치독 만료 슬롯 N개 × (3600/`TREX_STOP_LOSS_POLL_INTERVAL`) 호출이 KIS 분당/일일 쿼터를 초과할 수 있음. 대응: REQ-038 AC-8 상한 산정 + 임계 초과 시 경고 로그. 폴링 대상은 "워치독 만료" 슬롯으로 한정해 평시 호출 최소화.
- R-2 (Medium): WatchdogStore cross-thread read — safety-sell worker가 snapshot RPC handler thread의 write와 동시 접근. 대응: BAT-010 Key Decision 3(GIL 보장 단일 dict, lock 없음) 계약을 그대로 따르고 read-only 판정만 수행. 단위 테스트로 만료 판정 고정.
- R-3 (Medium): 장 마감 청산 발동 정밀도/중복 — worker 루프 주기에 의존하는 시각 평가 + 재시작 시 in-memory `last_liquidation_date` 초기화로 동일 거래일 재발동 위험. 대응: 발동 가드 + (필요 시) 발동 시각 경계 처리 설계에서 확정.
- R-4 (Low): KIS 거부/네트워크 예외 시 루프 안정성 — polling 또는 sell 제출 중 예외가 worker 스레드를 죽이면 안 됨. 대응: tick 단위 try/except + 로그 후 다음 tick 지속(REQ-024 degrade 정책 정합).
- R-5 (Low, 이월): BAT-010 잔여 R-2 — `004_add_price_thresholds.sql` 실 PostgreSQL round-trip 미검증. 대응: 본 batch verification에서 실 PG 적용 + round-trip 확인.
## Notes
- safety-sell worker는 REQ-037 Notes의 권고(forced_close·stop-loss/take-profit·장 마감을 하나의 통합 루프로 처리)를 따른다. 루프 주기와 가격 체크 주기가 다를 경우(`TREX_FORCED_CLOSE_CHECK_INTERVAL` vs `TREX_STOP_LOSS_POLL_INTERVAL`) tick 내 sub-scheduling 방식은 design에서 확정한다.
- REQ-040 OQ-6(non-holding 슬롯 처리)는 REQ-040 AC-6에서 `{status: ignored, reason: "not_holding"}` 응답으로 이미 결정됨 — 구현은 이를 따른다.
@@ -1,82 +0,0 @@
# BAT-011 Verification
- Status: verified
- Batch: BAT-011
## Acceptance Mapping
### REQ-037 (forced_close_at 만기 자동 매도 루프)
| AC | Evidence |
|----|---------|
| AC-1: 만기+워치독 만료 → sell() | `test_safety_sell_worker.py::TestForcedClose::test_expired_past_due_sells` |
| AC-2: 미만기(now+1h) → 미호출 | `test_future_not_due_no_sell` |
| AC-3: 워치독 활성 → 미호출(보호) | `test_watchdog_active_protected` |
| AC-4: forced_close_at None → 제외 | `test_no_forced_close_excluded` |
| AC-5: pending_sell 중복 미호출 | `test_idempotent_on_pending_sell` (transition_log PENDING_SELL 1건) |
| AC-6: interval 주기 | `TestRunOnce::test_base_tick_is_finest_interval` + `safety_sell.base_tick_sec`/`run_once` `_due` 캐던스(코드) |
| grace 보호 | `test_grace_period_defers` |
### REQ-038 (stop-loss·take-profit KIS polling + 트리거)
| AC | Evidence |
|----|---------|
| AC-1: stck_prpr ≤ stop_loss → 손절 | `TestPriceTriggers::test_stop_loss_triggers` |
| AC-2: stck_prpr ≥ take_profit → 익절 | `test_take_profit_triggers` |
| AC-3: 두 조건 미충족 → 미호출 | `test_neither_condition_no_sell` |
| AC-4: 워치독 활성 → KIS 호출 0 | `test_watchdog_active_no_kis_call` (broker.calls == []) |
| AC-5: 임계값 None → 제외 | `test_no_thresholds_excluded` |
| AC-6: pending_sell 중복 미호출 | `test_idempotent_on_pending_sell` |
| AC-7 (CHG-04): 시세 tr `FHKST01010100`(실전/모의 공용) + stck_prpr | `test_kis_price.py::test_tr_price_is_quotations_tr` + `test_get_current_price_parses_stck_prpr`; **실 KIS LIVE 검증**: `get_current_price('005930')=Decimal('329000')`(rt_cd=0). 구 `TTTC8397R` 는 LIVE rt_cd=1 "없는 서비스 코드" 로 폐기 |
| AC-8: 시간당 호출 ≤ N×(3600/interval) | `test_quota_estimate`(N=3,interval=30→360) + `test_only_eligible_slots_polled`(워치독 만료+임계 슬롯만 poll) |
| 가격 미수신 → skip | `test_price_unavailable_skips` |
### REQ-039 (장 마감 강제 청산)
| AC | Evidence |
|----|---------|
| AC-1: 시각 도래 → 전체 holding sell() | `TestMarketClose::test_time_reached_liquidates_all` |
| AC-2: 워치독 활성 포함 청산 | `test_watchdog_active_included` |
| AC-3: 미설정 → 미동작 | `test_not_configured_no_sell` |
| AC-4: 거래일 1회 발동 가드 | `test_once_per_day_guard` |
| AC-5: pending_sell 중복 미호출 | REQ-036 idempotency(`SlotService.sell`) — `test_service_sell.py::test_sell_idempotent_on_pending_sell` 승계, 청산 경로 동일 sell() 사용 |
| AC-6: grace period 무관 청산 | `test_grace_period_does_not_block` |
| 발동 시각 이전 → 미동작 | `test_before_time_no_sell` |
### REQ-040 (update_thresholds intent RPC)
| AC | Evidence |
|----|---------|
| AC-1: stop_loss_price 갱신 | `test_update_thresholds_handler.py::test_updates_stop_loss` |
| AC-2: take_profit + forced_close 갱신 | `test_updates_take_profit_and_forced_close` |
| AC-3: last_seen_at 리셋 | `test_resets_watchdog_last_seen` |
| AC-4: 부분 갱신(미포함 유지) | `test_partial_update_keeps_other_fields` |
| AC-5: null → 해제 | `test_explicit_null_clears` |
| AC-6: non-holding → ignored/not_holding | `test_non_holding_ignored` |
| AC-7: reply_to → updated 응답 | `test_reply_updated_status` |
| 미존재 슬롯 → ignored/slot_not_found | `test_unknown_slot_ignored` |
### REQ-042 (baseline 문서 정합)
| AC | Evidence |
|----|---------|
| AC-1: interface-contract update_thresholds 계약 | `docs/interface-contract.md` `trex.intent.req update_thresholds 액션` 블록 + Patterns 행 |
| AC-2: interface-contract price 선택 필드 | buy payload 에 `stop_loss_price`/`take_profit_price` 추가 |
| AC-3: runtime-flows 4종 흐름 | `docs/runtime-flows.md` 워치독 갱신/forced_close/가격 체크/장 마감 흐름 4종 추가 |
| AC-4: runtime-flows A-5 주석 갱신 | broker degrade 흐름의 "A-5 미구현" → dcy-007 REQ-037~041 해소로 갱신 |
| AC-5: project-structure trex-safety-sell 스레드 | `docs/project-structure.md` 7 스레드 목록 + safety_sell.py/watchdog.py 모듈 + 변경 로그 |
## Evidence
- Test:
- 신규 테스트 38종 green (test_kis_price 7 + test_safety_sell_worker 23 + test_update_thresholds_handler 8).
- 단위 전체 회귀: `236 passed / 23 skipped`(`::test_sandbox_token_and_submit` deselect — `.env=LIVE` 산물).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `259 passed`(마이그레이션 001~004 적용 + PG repo round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
- Manual:
- 없음(트리거 로직은 결정적 단위 테스트로 커버; 실 KIS 라이브 트리거는 운영 실증 이월).
- Logs:
- `236 passed, 23 skipped, 1 deselected in ~0.8s` / `259 passed ... in ~11s`.
## Carried-Over Items
- BAT-010 R-2 (실 PostgreSQL `004_add_price_thresholds.sql` round-trip): **해소** — 인프라 게이트 259 passed(마이그레이션 적용 + PG repo 테스트 green)로 확인.
- 운영 실증 이월(차기 거래일): stop-loss·take-profit·forced_close·장 마감의 실 KIS 라이브 트리거 1건 관찰. 휴장/운영 제약으로 본 batch 는 mock·정적 검증까지. release feedback 으로 추적.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 잔여(실 KIS 라이브 트리거 운영 실증)는 blocker 아님 — paper/mock 결정적 검증 + KIS 어댑터 tr_id/파싱 단위 검증으로 release gate 충족. 차기 거래일 운영 관찰로 보강(REL feedback).
@@ -1,56 +0,0 @@
# BAT-012 Design
- Status: confirmed
- Batch: BAT-012
## Architecture Summary
BAT-012 는 운영자 비상 정지(kill-switch)를 기존 인프라 위에 추가한다.
1. **제어 채널 일원화** (`trex.control.req`): 기존 `trex.pause.req` 단일 목적 큐를 `action` discriminator 기반 제어 채널로 확장. `make_control_rpc_handler` 가 pause/resume/list(REQ-032 위임) + kill_switch(REQ-045) + resume_kill_switch(REQ-046)를 분기. daemon 의 pause consumer 스레드를 control consumer 로 교체(스레드 수 7 불변).
2. **day-scoped 제외 store** (`killswitch_exclusion` + `PostgreSQLKillSwitchStore`): `(portfolio_id, trading_date)` 키. `snapshot_pause`(영구 pause)와 분리. `list_excluded(today)` 가 당일 항목만 반환 → day-scoped 자동 복귀.
3. **kill_switch 실행**: 범위별 holding 조회 → 순차 `SlotService.sell()` → 당일 제외 표시 → 이력 로깅. dcy-007 safety-sell 과 독립(REQ-036 idempotency 로 충돌 무해).
4. **masking 합성 + resume**: snapshot envelope masking 에 `killswitch_today` 추가. `resume_kill_switch` 가 day-scoped 제외 row 삭제(청산된 슬롯은 비가역).
## Changed Areas
- Docs:
- `docs/interface-contract.md` — 제어 채널 `trex.control.req` + action 목록 + kill_switch/resume_kill_switch payload/응답 + masking 소스 (REQ-047)
- `docs/runtime-flows.md` — kill-switch 발동/masking/자동복귀/resume 흐름 (REQ-047)
- `docs/project-structure.md` — control consumer + killswitch_exclusion 테이블/store + 005 sql (REQ-047)
- Modules:
- `trex_core/adapters/messaging/topology.py` — `CONTROL_REQ_QUEUE = "trex.control.req"` (PAUSE_REQ_QUEUE 대체/별칭)
- `trex_core/application/messaging_handlers.py` — `make_control_rpc_handler`(action dispatch) + kill_switch / resume_kill_switch handler
- `trex_core/adapters/persistence/sql/005_create_killswitch_exclusion.sql` (신규)
- `trex_core/adapters/portfolio/killswitch_store.py` — `PostgreSQLKillSwitchStore`
- `trex_core/application/portfolio.py`(envelope builder) — masking 소스에 `list_excluded(today)` 추가
- `trex_core/application/killswitch.py`(신규, 후보) 또는 `domain/slot/service.py` — 범위별 holding 순차 sell() 진입점
- `trex_core/runtime/daemon.py` — pause consumer → control consumer 교체 + killswitch_store 배선 + snapshot/control handler 주입
- Interfaces:
- `trex.control.req` action 계약: `pause|resume|list|kill_switch|resume_kill_switch`
- kill_switch payload `{action, scope: portfolio|global, portfolio_id?, actor?}` → `{status, scope, portfolio_id?, liquidated}`
- resume_kill_switch payload `{action, scope, portfolio_id?, actor?}` → `{status, scope, portfolio_id?}`
- `KillSwitchStore.exclude/clear/list_excluded(trading_date)`
## Key Decisions (초안 — 구현 전 확정 대상)
1. **제어 큐 전환 방식**: `trex.control.req` 단일 채널로 일원화. 기존 `trex.pause.req` 하위호환은 (a) 별칭 큐 동시 선언 유지 vs (b) control.req 단일화 + 클라이언트 갱신 중 택1 — release rollout 영향이라 확정 필요(R-1). 기본안: control.req 단일화(RPC pause 사용처는 전략/운영 한정, CLI 무영향).
2. **전역 제외 표현**: `killswitch_exclusion.portfolio_id = '*'` sentinel(기본안) vs scope 컬럼 분리. masking 소비처(REQ-046)와 계약만 일치하면 됨. 기본안: `'*'` sentinel + `list_excluded` 가 sentinel 포함 시 전체 마스킹 신호로 반환.
3. **kill_switch 실행 진입점**: 신규 `application/killswitch.py`(범위별 holding 조회 + 순차 sell() + store exclude + 로깅)로 분리(기본안). domain service 는 단일 슬롯 책임 유지, 복수 슬롯 오케스트레이션은 application 계층.
4. **trading_date 기준**: 데몬 기존 관례(UTC date) 사용 — REQ-045 표시·REQ-046 조회·자동복귀 모두 동일 키. (safety-sell `_trading_date(now)` 와 일치)
5. **dcy-007 독립성**: kill_switch 는 safety-sell worker 스레드와 상태/락 공유 없음. 동일 슬롯 동시 sell() 은 REQ-036 PENDING_SELL idempotency 로 1회만 전이.
## Edge Cases
- kill_switch scope=portfolio + portfolio_id 누락 → `{status:error, reason:portfolio_id_required}`.
- 대상 중 이미 pending_sell → sell() 건너뜀(idempotent), liquidated 카운트에서 제외 여부는 구현 결정(기본: 신규 전이만 카운트).
- resume_kill_switch 후 이미 청산된 슬롯 → 상태 불변(매도 비가역). resume 은 제외 row 만 삭제.
- 다음 거래일 → `list_excluded(D+1)` 에 전일 row 미포함 → 자동 복귀(별도 동작 불필요).
- 데몬 재시작(같은 거래일) → store 영속 → masking 유지(NFR-3).
- unknown action → `{status:error, reason:unknown_action:<a>}`.
## Architecture Impact
- runtime-flows: 제어 채널 + kill-switch 4흐름 추가 — REQ-047 에서 문서화.
- project-structure: control consumer 전환 + 신규 store/테이블/sql — REQ-047 에서 문서화.
- interface-contract: pause→control 채널 계약 갱신 — REQ-047.
## Reference Doc Update Plan
- `interface-contract.md`: 제어 채널 action 목록 + kill_switch/resume_kill_switch 계약 + masking 소스 (REQ-047 AC-1/2)
- `runtime-flows.md`: 발동/masking/자동복귀/resume 흐름 (REQ-047 AC-3)
- `project-structure.md`: control consumer + killswitch_exclusion/store + 005 sql (REQ-047 AC-4)
@@ -1,51 +0,0 @@
# BAT-012 Implementation
- Status: completed
- Batch: BAT-012
## Plan Summary
- 구현 순서(planning Delivery Plan): REQ-044 store/마이그레이션 → REQ-043 제어 채널/dispatch → REQ-045 kill_switch 실행 → REQ-046 masking/resume → REQ-047 baseline 정합. 각 단계 unit test green 후 진행.
## Changed Files
- (구현 시 기록)
### 신규 파일 (예정)
- `trex_core/adapters/persistence/sql/005_create_killswitch_exclusion.sql` (REQ-044)
- `trex_core/adapters/portfolio/killswitch_store.py` — `PostgreSQLKillSwitchStore` (REQ-044)
- `trex_core/application/killswitch.py` — 범위별 holding 순차 sell() 진입점 (REQ-045)
- `tests/adapters/portfolio/test_killswitch_store.py` (REQ-044)
- `tests/application/test_control_dispatch.py` (REQ-043)
- `tests/application/test_killswitch_execution.py` (REQ-045)
- `tests/application/test_killswitch_masking_resume.py` (REQ-046)
### 기존 파일 수정 (예정)
- `trex_core/adapters/messaging/topology.py` — CONTROL_REQ_QUEUE (REQ-043)
- `trex_core/application/messaging_handlers.py` — make_control_rpc_handler + kill_switch/resume_kill_switch (REQ-043/045/046)
- `trex_core/application/portfolio.py` — masking 소스 확장 (REQ-046)
- `trex_core/runtime/daemon.py` — control consumer 전환 + killswitch_store 배선 (REQ-043/045/046)
- `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/project-structure.md` (REQ-047)
## Changed Files (실제)
- 신규: `sql/005_create_killswitch_exclusion.sql`, `adapters/portfolio/killswitch_store.py`(PostgreSQLKillSwitchStore), `application/killswitch.py`(KillSwitchService), 테스트 4개(test_killswitch_store / test_control_dispatch / test_killswitch_execution / test_killswitch_masking_resume).
- 수정: `adapters/messaging/topology.py`(CONTROL_REQ_QUEUE), `adapters/messaging/rabbitmq.py`(control 큐 declare), `application/portfolio.py`(GLOBAL_KILLSWITCH + KillSwitchStore Protocol + InMemoryKillSwitchStore + envelope masking), `application/messaging_handlers.py`(make_control_rpc_handler + kill_switch/resume_kill_switch + snapshot handler killswitch_store), `runtime/daemon.py`(control consumer 전환 + KillSwitchService + killswitch_store 배선), `tests/conftest.py`(killswitch_exclusion truncate), baseline 3문서(REQ-047).
## Execution Log
1. REQ-044 store + 005 마이그레이션 → 단위 5 passed(PG 5 gated).
2. REQ-045 KillSwitchService → 단위 6 passed.
3. REQ-043 control dispatch(topology+handler+daemon) → 단위 9 passed, import/회귀 green.
4. REQ-046 masking + resume → 단위 6 passed.
5. REQ-047 baseline 3문서 정합.
6. 전체 회귀: 단위 262 passed / 28 skipped, ruff clean.
7. 인프라 게이트(PG+RabbitMQ): 290 passed(마이그레이션 001~005 + killswitch PG round-trip 포함).
8. paper 데몬 e2e(control RPC): buy 2 → `kill_switch global`(liquidated=2) → 양쪽 `holding→pending_sell(emergency_stop)→closed`, `killswitch_exclusion=[('*',2026-06-07)]` → `resume_kill_switch global` → exclusion 0. PASS.
## Validation
- 단위: `pytest -q --deselect ...::test_sandbox_token_and_submit` → 262 passed / 28 skipped.
- 인프라 게이트: `TREX_RABBITMQ_TEST_URL=... TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) pytest -q` → 290 passed.
- lint: `ruff check trex_core tests` → All passed.
- e2e: 위 8 (paper 데몬 control RPC kill_switch/resume).
## Remaining Risks
- R-1(제어 큐 rename 하위호환): control.req 단일화 구현. 기존 `trex.pause.req` 사용 RPC 클라이언트 이전은 release rollout 결정(REQ-043 Notes). CLI 무영향.
- R-5 해소: `005` 실 PG round-trip = 인프라 게이트 290 passed(killswitch PG 테스트 green).
- (이월) 실 KIS LIVE kill_switch 발동→실매도 제출→에러 관찰은 차기 거래일 운영 실증(safety-sell LIVE 선례). paper/mock 으로 충분 검증.
@@ -1,35 +0,0 @@
# BAT-012: 비상 정지/kill-switch — 제어 채널 + 범위별 청산 + 당일 제외 masking/resume
- Status: released
- Profile: standard
- Source Discovery: dcy-008
- Included REQ: REQ-043, REQ-044, REQ-045, REQ-046, REQ-047
- Excluded REQ: REQ-030 (BAT-008 released 포함, 옵션 D 는 portfolio-hub#8 후 별도 change-req)
## Summary
- dcy-008(비상 정지/강제 청산 kill-switch) 전체를 단일 standard batch 로 delivery 한다. 운영자가 `trex.control.req` 제어 채널로 portfolio/전역 단위 holding 슬롯을 즉시 청산하고 당일 전략 실행에서 제외하며(다음 거래일 자동 복귀 + 수동 resume), 기존 `SlotService.sell()`(REQ-036)·outbox·execution 인프라를 재사용한다. 구성: 제어 채널 + action dispatch(REQ-043), day-scoped 제외 store + 마이그레이션(REQ-044), kill_switch 실행(REQ-045), 당일 제외 masking + 자동복귀 + resume_kill_switch(REQ-046), baseline 3문서 정합(REQ-047).
## Scope
- In Scope:
- `trex.pause.req` → `trex.control.req` 제어 채널 확장 + `action` dispatch(pause/resume/list 유지 + kill_switch/resume_kill_switch 추가) (REQ-043)
- 신규 `killswitch_exclusion` 테이블 + `005_*.sql` additive 마이그레이션 + 전용 store 어댑터 (REQ-044)
- kill_switch 실행: 범위별(portfolio/전역) holding 순차 `SlotService.sell()` + REQ-036 idempotency + 발동 이력 로깅 (REQ-045)
- 당일 제외 masking(snapshot envelope `mask = pending|warning|pause|killswitch_today`) + 다음 거래일 자동 복귀 + `resume_kill_switch` 수동 해제 (REQ-046)
- baseline 3문서(interface-contract/runtime-flows/project-structure) 정합 (REQ-047)
- Out of Scope:
- pending_buy 슬롯 롤백 (dcy-008 OQ-3 Deferred — 기본 구현 HOLDING 한정)
- 부분 수량 청산 (GitHub #5 별도 Discovery)
- kill-switch 발동 후 자율 안전 매도(dcy-007/REL-003) 비활성화 — 두 경로 독립
- 제어 큐 rename 의 기존 클라이언트 호환 정책 확정 — release 단계 결정(REQ-043 Notes)
## 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-008). OQ-1(제어 채널)·OQ-2(별도 테이블)·OQ-4(수동 resume) Answered, OQ-3(pending_buy) REQ Deferred.
- NFR-1(청산 지연)·NFR-2(dcy-007 독립)는 REQ-045 AC-7/8, NFR-3(영속 masking)은 REQ-046 AC-5 로 흡수(별도 NFR REQ 없음).
- 기존 자산 재사용: `SlotService.sell()`(REQ-036/BAT-010), outbox·execution worker, pause masking 패턴(`portfolio.py`). 호출 진입점·제어 채널·day-scoped store 만 신규.
- REQ-047(문서 정합)은 REQ-043~046 구현 완료 후 verification 단계 수행(REQ-042 선례).
@@ -1,52 +0,0 @@
# BAT-012 Planning
- Status: confirmed
- Batch: BAT-012
- Profile: standard
## Included REQ
- REQ-043 (Interface): `trex.pause.req` → `trex.control.req` 제어 채널 + `action` dispatch. pause/resume/list 회귀 보존 + kill_switch/resume_kill_switch 라우팅. unknown action error. 워커 스레드 수 불변(pause→control 교체).
- REQ-044 (Data): `killswitch_exclusion(portfolio_id, trading_date, actor, created_at)` 테이블 + `005_*.sql` additive 마이그레이션 + `PostgreSQLKillSwitchStore`(exclude/clear/list_excluded(day)). 전역 sentinel.
- REQ-045 (Interface): kill_switch handler — payload `{action, scope, portfolio_id?}` → 범위별 holding 순차 `SlotService.sell(actor="kill-switch", reason="emergency_stop")` + REQ-036 idempotency + 당일 제외 표시(044) + 발동 이력 로깅 + 응답 `{liquidated:N}`.
- REQ-046 (Interface): snapshot masking 소스에 `list_excluded(today)` 추가(portfolio/전역) + day-scoped 자동 복귀 + `resume_kill_switch` handler(`clear`) — 청산된 슬롯은 비가역.
- REQ-047 (Documentation): interface-contract(제어 채널+action+payload), runtime-flows(발동/masking/복귀/resume 흐름), project-structure(control consumer + killswitch_exclusion + store + 005 sql) 정합.
## Delivery Plan
1. REQ-044: 마이그레이션 `005_create_killswitch_exclusion.sql` + `PostgreSQLKillSwitchStore` + 단위/PG 게이트 테스트(day-scoped/멱등/전역 sentinel).
2. REQ-043: `topology.CONTROL_REQ_QUEUE` + `make_control_rpc_handler`(action dispatch, pause/resume/list 위임 + kill_switch/resume_kill_switch 라우팅) + `daemon.py` pause consumer→control consumer 교체. dispatch 단위 테스트(회귀 + 라우팅 + unknown).
3. REQ-045: kill_switch handler + 범위별 holding 조회·순차 sell() 진입점(application service 또는 service 헬퍼) + 044 store exclude 호출 + 이력 로깅. 단위 테스트(전역/portfolio/idempotency/portfolio_id 누락/이력/독립성/지연).
4. REQ-046: snapshot envelope masking 확장(`killswitch_today`) + resume_kill_switch handler(clear) + day-scoped 자동복귀. 단위 테스트(masking/추가매수거부/자동복귀(날짜 mock)/재시작 영속/resume).
5. REQ-047: baseline 3문서 정합(구현 완료 상태 반영).
6. 전체 회귀 + ruff + 인프라 게이트(PG: 005 round-trip, RabbitMQ: control RPC) + e2e(발동→청산→masking→resume).
## Dependencies
- Internal:
- REQ-036/BAT-010 `SlotService.sell()` — kill_switch 청산 경로(재사용).
- REQ-032/BAT-008 `PostgreSQLPauseStore` + pause masking 패턴(`portfolio.py`) — masking 합성 참조(분리 유지).
- REQ-022/BAT-007 `make_*_rpc_handler` + consume_rpc_forever — control consumer 전환 대상.
- BAT-008 snapshot envelope builder — masking 소스 확장 지점.
- External:
- 없음 (kill_switch 청산은 기존 broker 경로 사용; 신규 외부 API 없음).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) **제어 채널 전환**(`trex.pause.req`→`trex.control.req`) 은 인터페이스·daemon consumer 구조 변경이며 기존 pause 클라이언트 하위호환(별칭 vs 단일화) 결정이 design 에서 필요하다. (b) **day-scoped store 의 전역 sentinel 표현**과 masking 합성 계약(REQ-044↔046) 을 설계에서 고정해야 구현 drift 가 없다. (c) **kill_switch 실행 진입점 배치**(domain service vs application service)와 발동 이력/순차 처리 모델, dcy-007 safety-sell 과의 독립성 보장을 설계에서 명시해야 한다. → `design.md` 동일 턴 scaffold.
## Milestones
1. M1 — REQ-044 store + 마이그레이션 + 단위/PG 테스트 green
2. M2 — REQ-043 control 채널 + dispatch + pause/resume/list 회귀 green
3. M3 — REQ-045 kill_switch 실행 + idempotency + 이력 단위 green
4. M4 — REQ-046 masking + 자동복귀 + resume 단위 green
5. M5 — REQ-047 baseline 3문서 정합
6. M6 — 전체 회귀 + ruff + 인프라 게이트 + e2e(발동→청산→masking→resume)
## Risks
- R-1 (Medium): 제어 큐 rename(`pause.req`→`control.req`) 하위호환 — 기존 RPC pause 클라이언트 영향. 대응: design 에서 별칭 유지 vs 단일화 결정, release rollout 에 명시. CLI pause/resume 는 store 직접 호출이라 무영향.
- R-2 (Medium): kill_switch 다수 슬롯 순차 sell() 큐 부하(dcy-008 R-1). 대응: sell()=PENDING_SELL 전이(DB write 1회) 빠름, N<10 가정(NFR-1).
- R-3 (Low): day-scoped 전역 sentinel 표현 불일치(044 store ↔ 046 masking). 대응: design 에서 계약 고정 + 단위 테스트.
- R-4 (Low): kill_switch ↔ 자율 안전 매도(dcy-007) 동시 sell() 중복. 대응: REQ-036 idempotency(PENDING_SELL 중복 무시).
- R-5 (Low): `005_*.sql` 실 PG round-trip(BAT-010 R-2 선례). 대응: verification 인프라 게이트에서 확인.
## Notes
- pending_buy 슬롯 처리(OQ-3)는 본 batch 범위 밖(HOLDING 한정). 필요 시 후속 REQ/change-req.
- 제어 큐 호환 최종 결정은 release(REL) 단계 rollout 에서 확정(REQ-043 Notes).
@@ -1,80 +0,0 @@
# BAT-012 Verification
- Status: verified
- Batch: BAT-012
## Acceptance Mapping
> 구현 완료. evidence: 신규 단위 26 + PG 게이트(killswitch store 5) + e2e. 파일: test_control_dispatch.py(9), test_killswitch_execution.py(6), test_killswitch_masking_resume.py(6+1), test_killswitch_store.py(in-mem 5 + PG 5).
### REQ-043 (제어 채널 + action dispatch)
| AC | Evidence (예정) |
|----|---------|
| AC-1: pause 회귀 | control dispatch 단위(pause) |
| AC-2: resume/list 회귀 | 동일 |
| AC-3: kill_switch 라우팅 | 동일 |
| AC-4: resume_kill_switch 라우팅 | 동일 |
| AC-5: unknown action error | 동일 |
| AC-6: 워커 스레드 7 불변 | daemon 구조 검사/test_daemon |
### REQ-044 (day-scoped 제외 store + 마이그레이션)
| AC | Evidence (예정) |
|----|---------|
| AC-1: 테이블 스키마 | 마이그레이션 + PG 게이트 |
| AC-2: 마이그레이션 멱등 | 005 IF NOT EXISTS + 재적용 |
| AC-3: exclude→list_excluded | store 단위 |
| AC-4: day-scoped 자동 제외 | list_excluded(D+1) |
| AC-5: 멱등 exclude | store 단위 |
| AC-6: clear | store 단위 |
| AC-7: 전역 sentinel | store 단위 |
### REQ-045 (kill_switch 실행)
| AC | Evidence (예정) |
|----|---------|
| AC-1: 전역 전체 청산 | killswitch 실행 단위 |
| AC-2: portfolio 범위 한정 | 동일 |
| AC-3: pending_sell idempotency | 동일 |
| AC-4: portfolio_id 누락 error | 동일 |
| AC-5: 당일 제외 표시 | 044 store 연동 |
| AC-6: 이력 로깅 | 로그 캡처 |
| AC-7: dcy-007 독립 | 동일 |
| AC-8: 지연 N×100ms+5s | 측정/코드 검사 |
### REQ-046 (masking + 자동복귀 + resume)
| AC | Evidence (예정) |
|----|---------|
| AC-1: portfolio masking | masking 단위 |
| AC-2: 전역 masking | 동일 |
| AC-3: 추가매수 거부 | buyable masking |
| AC-4: 다음 거래일 자동복귀 | 날짜 mock |
| AC-5: 재시작 영속 masking | 통합/날짜 mock |
| AC-6: resume_kill_switch | resume 단위 |
| AC-7: portfolio_id 누락 error | 동일 |
| AC-8: 청산 슬롯 비가역 | 상태 불변 확인 |
### REQ-047 (baseline 정합)
| AC | Evidence (예정) |
|----|---------|
| AC-1: interface-contract 제어 채널+action | 문서 diff |
| AC-2: interface-contract kill/resume payload | 문서 diff |
| AC-3: runtime-flows 4흐름 | 문서 diff |
| AC-4: project-structure control consumer + store/sql | 문서 diff |
## Evidence
- Test:
- 신규 단위 26 + PG 게이트 killswitch store 5 green. (control dispatch 9 / 실행 6 / masking·resume 7 / store in-mem 5 / store PG 5)
- 단위 전체 회귀: `262 passed / 28 skipped`(`::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `290 passed`(마이그레이션 001~005 적용 + killswitch PG round-trip 포함).
- `ruff check trex_core tests` → All passed.
- Manual / e2e:
- paper 데몬 control RPC e2e: buy 2(pf-A/pf-B) → `{action:kill_switch, scope:global}` → `{status:ok, liquidated:2}` → 양쪽 `holding→pending_sell(emergency_stop)→closed`, `killswitch_exclusion=[('*',2026-06-07)]` → `{action:resume_kill_switch, scope:global}` → exclusion 0. PASS.
- Logs:
- `262 passed, 28 skipped` / `290 passed ... ~13s` / e2e `RESULT: PASS`.
## Carried-Over / e2e
- BAT-010 R-2 선례(실 PG 마이그레이션 round-trip): `005` 인프라 게이트 290 passed 로 확인.
- (이월, 비차단) 실 KIS LIVE kill_switch 발동 → 실매도 제출 → 상황별 에러(주말/휴장) 관찰 — safety-sell LIVE e2e 선례와 동일 접근, 차기 거래일 운영 실증. paper/mock + 290 passed 로 release gate 충족.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 제어 큐 rename 하위호환(R-1)은 release rollout 결정 항목(REQ-043 Notes), 실 KIS 라이브 발동은 운영 실증 이월 — 둘 다 blocker 아님.
@@ -1,51 +0,0 @@
# BAT-013 Design
- Status: confirmed
- Batch: BAT-013
## Architecture Summary
다중 인스턴스가 동일 RabbitMQ 에서 split-brain 없이 운영되도록 토폴로지·설정을 인스턴스화한다.
1. **동적 큐명**(`topology.py`): `intent_queue/snapshot_queue/control_queue(instance_key)`. `instance_key in (None,"default")` → 기존 리터럴(`trex.intent.req` 등), 그 외 → `{base}.{instance_key}`. 기존 상수는 default 값으로 보존(하위호환).
2. **exchange**(`topology.py`+`rabbitmq.py`): `trex.direct`(topic,durable) + `trex.broadcast`(fanout,durable). `declare_topology(instance_key)` 가 인스턴스 큐를 direct 에 `{instance_key}.{intent|control|snapshot}` 로 바인딩 + control 큐를 broadcast 에 dual 바인딩.
3. **설정**(`config.py`): `DaemonInstanceConfig(instance_key, portfolio_hub_token)` + `from_toml(path)`(tomllib) + `DaemonConfig.from_config(toml_path)` 가 `from_env()` 결과에 TOML 을 병합(`portfolio_hub_token` → PortfolioHubConfig.token).
4. **진입점**(`daemon.main`): argparse `--config` 필수. instance_key 를 declare/consume 에 일관 전달.
## Changed Areas
- Docs: interface-contract.md(routing key)·runtime-flows.md(TOML 기동/라우팅)·project-structure.md(동적 큐명/설정/exchange) — REQ-051.
- Modules:
- `trex_core/adapters/messaging/topology.py` — 큐명 빌더 + DIRECT/BROADCAST exchange 상수 + `direct_routing_key`
- `trex_core/adapters/messaging/rabbitmq.py` — `declare_topology(instance_key="default")` + exchange 선언 + binding(direct per-instance + control dual) + (필요시) broadcast publish helper
- `trex_core/runtime/config.py` — `DaemonInstanceConfig` + `from_toml` + `from_config`
- `trex_core/runtime/daemon.py` — `--config` argparse(main) + instance_key 보관 + consumer 큐명에 instance_key 적용 + declare_topology(instance_key)
- `.gitignore` — 인스턴스 TOML 패턴
- Interfaces:
- `intent_queue(instance_key) -> str` 등 (default 하위호환)
- `declare_topology(instance_key="default")`
- `direct_routing_key(instance_key, channel_type) -> "{instance_key}.{channel_type}"`
- `DaemonInstanceConfig.from_toml(path)`, `DaemonConfig.from_config(toml_path)`
## Key Decisions
1. **default = 리터럴 큐명(특수 처리)**: FR-2(suffix)와 FR-5(default 무회귀)의 긴장을 해소 — named instance 만 suffix, default 는 기존 이름 유지. 기존 messaging 테스트/배포 무변경. (planning R-1)
2. **declare_topology 기본 인자 `instance_key="default"`**: 기존 호출부(`transport.declare_topology()`)가 인자 없이도 default 동작 → 호출부 광범위 수정 회피.
3. **exchange 는 항상 선언(default 포함)**: direct/broadcast 는 default 인스턴스에서도 선언·바인딩하되, 기존 직접 큐 reply-to RPC 경로는 그대로 동작(추가 바인딩은 비파괴적). 기존 RPC 회귀 없음(FR-5/AC-6).
4. **TOML 우선 병합**: `from_config` 는 `from_env()` 로 베이스 구성 후 TOML 의 `portfolio_hub_token` 을 PortfolioHubConfig.token 으로 override. `instance_key` 는 TOML 전용.
5. **`--config` 필수**: argparse required. 누락 시 argparse 가 usage 출력 + exit≠0(OQ-2). 단, `from_env()` 단독 호출(테스트/라이브러리)은 계속 가능.
6. **pika per-thread**: 바인딩 추가만, consumer 스레드/커넥션 수 불변(NFR-2).
## Edge Cases
- default 큐명 충돌 없음: `*.default` 를 쓰지 않고 리터럴 사용.
- TOML 누락 필드/파일 부재 → 명확한 예외(FR-1/AC-4).
- broadcast 중복 control 전달 → 핸들러 idempotency(REQ-036/advisory lock, R-3).
- 기존 old 큐 잔류 → 자연 소멸(OQ-1), 코드 무관.
- snapshot/intent reply-to RPC: 클라이언트가 direct routing key 로 보내든 직접 큐로 보내든, 인스턴스 큐가 direct 바인딩 + 직접 publish 둘 다 수신 가능(하위호환).
## Architecture Impact
- runtime-flows: TOML 기동 + directed/broadcast 라우팅 + split-brain 방지 — REQ-051.
- project-structure: topology 동적화 + 설정 계층 + exchange — REQ-051.
- interface-contract: routing key 계약 — REQ-051.
## Reference Doc Update Plan
- interface-contract.md: routing key(`{instance_key}.{channel_type}`)+broadcast+큐명 규칙 (REQ-051 AC-1)
- runtime-flows.md: `--config` 기동 + directed/broadcast + split-brain (REQ-051 AC-2)
- project-structure.md: 동적 큐명/DaemonInstanceConfig/`--config`/exchange (REQ-051 AC-3)
@@ -1,45 +0,0 @@
# BAT-013 Implementation
- Status: completed
- Batch: BAT-013
## Plan Summary
- 순서: REQ-049 동적 큐명/declare_topology → REQ-050 exchange/binding/routing → REQ-048 TOML 로더/--config → REQ-051 baseline. 각 단계 단위 green 후 진행. default 하위호환 회귀 우선.
## Changed Files
- (구현 시 기록)
### 신규 (예정)
- `tests/runtime/test_instance_config.py` (REQ-048)
- `tests/adapters/messaging/test_instance_topology.py` (REQ-049/050)
- 인스턴스 TOML 예시(`config/instance.example.toml`, 추적) (REQ-048)
### 수정 (예정)
- `trex_core/adapters/messaging/topology.py` (동적 큐명 + DIRECT/BROADCAST + routing key)
- `trex_core/adapters/messaging/rabbitmq.py` (declare_topology(instance_key) + exchange/binding)
- `trex_core/runtime/config.py` (DaemonInstanceConfig + from_toml + from_config)
- `trex_core/runtime/daemon.py` (--config argparse + instance_key 큐 적용)
- `.gitignore` (인스턴스 TOML)
- baseline 3문서 (REQ-051)
## Changed Files (실제)
- 신규: `config/instance.example.toml`(추적), `tests/runtime/test_instance_config.py`, `tests/adapters/messaging/test_instance_topology.py`.
- 수정: `topology.py`(동적 큐명 빌더 + DEFAULT_INSTANCE_KEY/DIRECT_EXCHANGE/BROADCAST_EXCHANGE + direct_routing_key), `rabbitmq.py`(declare_topology(instance_key) + exchange/binding/control dual + import 정리), `config.py`(DaemonInstanceConfig+from_toml+from_config, DaemonConfig.instance_key), `daemon.py`(--config argparse main + per-instance 큐 구독 + declare_topology(instance_key)), `.gitignore`(인스턴스 TOML), baseline 3문서(REQ-051).
## Execution Log
1. REQ-049 동적 큐명(default 리터럴) + declare_topology(instance_key) + daemon per-instance consume → 빌더 단위 + messaging 회귀 green.
2. REQ-050 trex.direct/trex.broadcast exchange + binding + routing key → 인프라 게이트(directed 격리 + broadcast dual) green.
3. REQ-048 DaemonInstanceConfig.from_toml + from_config 병합 + --config argparse + .gitignore + example TOML → 로더 단위 + `--config` 누락 subprocess 종료 green.
4. REQ-051 baseline 3문서 정합.
5. 전체 회귀: 단위 271 passed / 30 skipped, ruff clean.
6. 인프라 게이트(PG+RabbitMQ): 301 passed (directed 라우팅 격리 S-2 + broadcast dual S-3 + default 무회귀 S-4 포함).
## Validation
- 단위: `pytest -q --deselect ...::test_sandbox_token_and_submit` → 271 passed / 30 skipped.
- 인프라 게이트: `TREX_RABBITMQ_TEST_URL=... TREX_POSTGRESQL_TEST_DSN=... pytest -q` → 301 passed.
- lint: `ruff check trex_core tests` → All passed.
## Remaining Risks
- R-1(declare_topology 변경 회귀): default=리터럴 큐명 하위호환으로 기존 messaging 테스트 무변경 — 301 passed 로 확인(FR-5/NFR-1).
- R-2(old 큐 잔류): 운영 자연 소멸(OQ-1), 코드 무관.
- (이월, 비차단) 실 2-인스턴스 동시 기동 manual e2e: directed 격리·broadcast 는 인프라 게이트 통합 테스트로 검증됨. 실 배포 시 다중 인스턴스 운영 시점 확인.
@@ -1,33 +0,0 @@
# BAT-013: 다중 인스턴스 RabbitMQ 토폴로지 — TOML config + instance_key 큐 + direct/broadcast exchange
- Status: released
- Profile: standard
- Source Discovery: dcy-009
- Included REQ: REQ-048, REQ-049, REQ-050, REQ-051
## Summary
- dcy-009(다중 인스턴스 RabbitMQ 토폴로지)를 단일 standard batch 로 delivery 한다. per-instance TOML 설정 로더 + `--config` 필수(REQ-048), `instance_key` 기반 동적 큐명 + default 하위호환(REQ-049), `trex.direct`(topic)/`trex.broadcast`(fanout) exchange + 바인딩(REQ-050), baseline 3문서 정합(REQ-051). 다중 인스턴스를 동일 RabbitMQ 에 붙여도 인스턴스 전용 큐로 split-brain(WatchdogStore/SafetySellService 경쟁 소비)을 구조적으로 방지한다.
## Scope
- In Scope:
- `DaemonInstanceConfig.from_toml` + `--config` 필수 진입점 + env 병합 + `.gitignore` TOML (REQ-048)
- `topology.py` 동적 큐명 빌더(`intent_queue/snapshot_queue/control_queue(instance_key)`, default=리터럴 하위호환) + `declare_topology(instance_key)` + daemon per-instance consume (REQ-049)
- `trex.direct`(topic) + `trex.broadcast`(fanout) exchange + per-instance/control dual binding + routing key 빌더 (REQ-050)
- baseline 3문서(interface-contract/runtime-flows/project-structure) (REQ-051)
- Out of Scope:
- strategy 모듈(orb-v1) publish 코드 변경 — 계약만 정의
- WatchdogStore Redis 외부화 / leader election / failover
- 전체 env 의 TOML 일원화(KIS/DSN/RABBITMQ_URL 은 `.env` 유지)
- old 큐 자동 삭제 스크립트(운영 절차)
## 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-009). OQ-1(old 큐 자연 소멸)·OQ-2(`--config` 필수) Answered.
- 하위호환 핵심(REQ-049): `instance_key="default"` → 리터럴 큐명 → 기존 테스트/배포 무회귀(FR-5/NFR-1).
- NFR-2(pika per-thread) 유지, NFR-3(split-brain) 인스턴스 전용 큐로 달성, NFR-4(민감값) TOML+gitignore.
- multi-instance 라우팅(directed/broadcast)·split-brain 격리는 인프라 게이트(RabbitMQ) 통합/e2e 로 검증.
@@ -1,48 +0,0 @@
# BAT-013 Planning
- Status: confirmed
- Batch: BAT-013
- Profile: standard
## Included REQ
- REQ-048 (Configuration): `DaemonInstanceConfig.from_toml` (tomllib) + `--config` 필수 진입점 + `from_config` env 병합(`portfolio_hub_token` 주입) + `.gitignore` TOML + 토큰 로그 미노출.
- REQ-049 (Interface): `topology.py` 동적 큐명 빌더(default=리터럴 하위호환) + `declare_topology(instance_key)` + daemon intent/snapshot/control consumer per-instance 큐 구독.
- REQ-050 (Integration): `trex.direct`(topic)/`trex.broadcast`(fanout) exchange + per-instance direct binding + control dual binding + `direct_routing_key` 빌더.
- REQ-051 (Documentation): interface-contract(routing key)/runtime-flows(TOML 기동·라우팅)/project-structure(동적 큐명·설정 계층·exchange) 정합.
## Delivery Plan
1. REQ-049: `topology.py` 큐명 빌더(default 하위호환) → `rabbitmq.declare_topology(instance_key)` → daemon consumer per-instance. 큐명 단위 + 기존 회귀.
2. REQ-050: `trex.direct`/`trex.broadcast` exchange 선언 + binding(direct per-instance, control dual) + routing key 빌더. 토폴로지 단위 + 인프라 게이트(라우팅 격리/broadcast).
3. REQ-048: `DaemonInstanceConfig.from_toml` + `--config` argparse 진입점 + `from_config` 병합 + `.gitignore` + 토큰 미노출. 로더 단위.
4. REQ-051: baseline 3문서 정합.
5. 전체 회귀 + ruff + 인프라 게이트(default 무회귀 + 라우팅) + (가능 시) 2-인스턴스 e2e.
## Dependencies
- Internal:
- dcy-006/BAT-007 messaging(topology/rabbitmq/daemon consumer) — 변경 대상 기반.
- BAT-012 control 채널(`trex.control.req`) — control 큐가 broadcast dual binding 대상.
- BAT-008 PortfolioHubConfig.token — TOML 주입 지점.
- External:
- RabbitMQ(topic/fanout exchange). stdlib `tomllib`(py3.12).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) `declare_topology` signature 변경 + 큐명 동적화는 **모든 messaging consumer/test 표면**에 영향 — default 하위호환 규칙(리터럴 vs suffix)을 설계에서 고정해야 회귀를 막는다. (b) topic/fanout exchange + per-instance/dual binding 의 정확한 binding 매트릭스와 routing key 계약을 설계에서 확정해야 strategy 계약과 일치한다. (c) TOML+env 병합 우선순위(`portfolio_hub_token` TOML 우선) + `--config` 진입점 구조를 설계에서 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-049 동적 큐명 + declare_topology(instance_key) + default 회귀 green
2. M2 — REQ-050 exchange + binding + routing key + 인프라 게이트(라우팅) green
3. M3 — REQ-048 TOML 로더 + --config + 병합 + .gitignore 단위 green
4. M4 — REQ-051 baseline 3문서
5. M5 — 전체 회귀 + ruff + 인프라 게이트 + 2-인스턴스 e2e(가능 시)
## Risks
- R-1 (Medium): `declare_topology`/큐명 변경이 기존 messaging 테스트 다수에 영향. 대응: default=리터럴 큐명 하위호환으로 기존 테스트 무변경 유지(REQ-049 핵심). 빌더에 default 특수 처리.
- R-2 (Medium): old durable 큐 잔류(dcy-009 R-2). 대응: 자연 소멸 허용(OQ-1), 배포 절차 외. 코드 무관.
- R-3 (Low): broadcast 중복 제어(dcy-009 R-3). 대응: kill-switch/pause idempotency(REQ-036/advisory lock).
- R-4 (Low): TOML 토큰 커밋 노출(dcy-009 R-4). 대응: `.gitignore` + 로그 미노출(REQ-048).
- R-5 (Low): pika BlockingConnection thread-safety. 대응: per-thread connection 유지(NFR-2), 바인딩만 추가.
## Notes
- 하위호환 우선: default 인스턴스는 기존 리터럴 큐명·RPC 경로 그대로 → 기존 277+ 테스트 무회귀가 1차 게이트.
- 2-인스턴스 e2e(라우팅 격리/broadcast)는 RabbitMQ 인프라 게이트 또는 manual; 단일 인스턴스 기본 검증은 단위로 충분.
@@ -1,64 +0,0 @@
# BAT-013 Verification
- Status: verified
- Batch: BAT-013
## Acceptance Mapping
> 구현 완료 후 각 AC 에 evidence 매핑. 아래는 계획 골격.
### REQ-048 (TOML 로더 + --config)
| AC | Evidence (예정) |
|----|---------|
| AC-1 from_toml 파싱 | 로더 단위 |
| AC-2 --config 누락 종료 | 진입점 단위/검사 |
| AC-3 token 주입 | 로더 단위(S-5) |
| AC-4 필수 필드 누락 오류 | 로더 단위 |
| AC-5 .gitignore TOML | gitignore 확인 |
| AC-6 token 로그 미노출 | 코드/로그 검사 |
### REQ-049 (동적 큐명 + 하위호환)
| AC | Evidence (예정) |
|----|---------|
| AC-1 named suffix | 큐명 빌더 단위 |
| AC-2 default 리터럴 | 동일 |
| AC-3 declare named 큐 | 인프라 게이트 |
| AC-4 default 회귀 없음 | 전체 pytest |
| AC-5 per-thread/스레드 수 | daemon 검사 |
| AC-6 인스턴스 격리 | 인프라 게이트/e2e |
### REQ-050 (exchange + binding)
| AC | Evidence (예정) |
|----|---------|
| AC-1 direct/broadcast 선언 | 인프라 게이트 |
| AC-2 direct binding | 인프라 게이트 |
| AC-3 routing key 빌더 | 단위 |
| AC-4 directed 격리 | 인프라 게이트(2큐) |
| AC-5 broadcast dual 전달 | 인프라 게이트(2큐) |
| AC-6 default 회귀 | 전체 pytest |
### REQ-051 (baseline 정합)
| AC | Evidence (예정) |
|----|---------|
| AC-1 interface-contract routing key | 문서 diff |
| AC-2 runtime-flows 기동/라우팅 | 문서 diff |
| AC-3 project-structure 동적큐/설정/exchange | 문서 diff |
## Evidence
- Test:
- REQ-048 로더 6 단위(from_toml/필수 누락/token 주입 S-5/default ik/`--config` 누락 subprocess 종료/gitignore) green.
- REQ-049/050 토폴로지 3 단위(builders default vs named/routing key) + 인프라 게이트 2(directed 격리 S-2 / broadcast dual S-3) green.
- 단위 전체 회귀: `271 passed / 30 skipped`(`::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `301 passed`(directed 라우팅 격리 + broadcast dual + default 무회귀 S-4 + 마이그레이션 round-trip 포함).
- `ruff check trex_core tests` → All passed.
- Logs: `271 passed, 30 skipped` / `301 passed ... ~15s`.
## Carried-Over / e2e
- S-1(named 큐 선언)·S-2(directed 격리)·S-3(broadcast dual): RabbitMQ 인프라 게이트 통합 테스트로 확인.
- S-4(default 무회귀): 전체 pytest 301 passed.
- S-5(TOML token 주입): 로더 단위.
- (이월, 비차단) 실 2-인스턴스 동시 기동 manual e2e: 실 다중 인스턴스 운영 시점 확인.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 제어 큐 rename(BAT-012 R-1)·old 큐 자연 소멸(OQ-1)·strategy publish 변경(Out of Scope)·2-인스턴스 manual e2e 는 비차단(인프라 게이트 통합으로 핵심 라우팅 검증됨).
@@ -1,30 +0,0 @@
# BAT-014 Implementation
- Status: completed
- Batch: BAT-014
## Plan Summary
- REQ-053(config 2종) → REQ-052(쿨다운 로직) → REQ-054(runtime-flows). 인메모리만, DB/스레드/인터페이스 변경 없음.
## Changed Files
- 수정:
- `trex_core/runtime/config.py` — `SafetySellConfig.min_retry_interval_sec`(기본 600)·`max_retry_warn_count`(기본 3) + `from_env` 파싱(`TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`/`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`) (REQ-053)
- `trex_core/application/safety_sell.py` — `_last_attempt`/`_attempt_count` dict, `_in_cooldown`/`_attempt_sell`/`_prune_cooldown`, `check_forced_close`·`check_prices` 쿨다운 gate(skip→KIS poll skip 포함)·경고, `run_once` 시작 시 prune. market_close 는 미적용 (REQ-052)
- `docs/runtime-flows.md` — safety-sell 재시도 쿨다운 flow + 가격체크 tr_id(FHKST01010100) 정정 (REQ-054)
- `tests/runtime/test_safety_sell_config.py` — 쿨다운 env 기본값/파싱 2종
- `tests/application/test_safety_sell_worker.py` — TestRetryCooldown 6종(skip/만료 재시도/경고/prune/market_close 제외/트리거 해소)
## Execution Log
1. REQ-053 config 2종 + from_env → `test_safety_sell_config.py` green.
2. REQ-052 쿨다운(`_in_cooldown`/`_attempt_sell`/`_prune_cooldown` + forced_close/prices gate) → TestRetryCooldown 6 green (non-transitioning sell stub 으로 영속 거부 시뮬).
3. REQ-054 runtime-flows 쿨다운 flow 추가.
4. 전체 회귀: 단위 278 passed / 30 skipped, ruff clean.
## Validation
- `pytest tests/application/test_safety_sell_worker.py tests/runtime/test_safety_sell_config.py` → 34 passed.
- 전체 `pytest -q --deselect ...::test_sandbox_token_and_submit` → 278 passed / 30 skipped.
- `ruff check trex_core tests` → All passed.
## Remaining Risks
- R-2(재시작 시 쿨다운 초기화 → 1회 즉시 재시도): 허용(문서 명시).
- 실 운영 거부 시나리오(휴장/잔고)에서의 쿨다운 동작은 차기 거래일 실 관찰로 보강(단위는 stub 로 시뮬). 인메모리·additive 변경이라 release 위험 낮음.
@@ -1,27 +0,0 @@
# BAT-014: safety-sell 영속 거부 재시도 쿨다운 (per-slot cooldown + 경고 + env)
- Status: released
- Profile: batch-lite
- Source Discovery: dcy-010
- Included REQ: REQ-052, REQ-053, REQ-054
## Summary
- dcy-010(safety-sell 영속 거부 재시도 백오프)를 batch-lite 로 delivery 한다. `SafetySellService`에 per-slot 인메모리 쿨다운(마지막 시도 시각·시도 횟수)을 추가해 영속 거부(휴장/잔고/규제) 시 `min_retry_interval_sec`(기본 600s) 이내 재시도를 건너뛰고, `max_retry_warn_count`(기본 3) 도달 시 운영자 WARNING 을 발행한다. `SafetySellConfig` env 2종 + runtime-flows 정합. **DB/스레드/인터페이스 변경 없음**(인메모리만).
## Scope
- In Scope:
- `SafetySellService` per-slot 쿨다운 추적(`_last_attempt`/`_attempt_count`) + skip/만료 재시도 + 경고 로그 + holding 이탈 정리, market_close 제외 (REQ-052)
- `SafetySellConfig` `min_retry_interval_sec`/`max_retry_warn_count` env 2종 (REQ-053)
- `docs/runtime-flows.md` safety-sell tick 쿨다운 단계 반영 (REQ-054)
- Out of Scope:
- ExecutionService→SafetySellService 거부 사유 피드백 채널, 거부 원인별 차등 쿨다운, 쿨다운 DB 영속, 수동 해제 API, market_close 별도 쿨다운.
## Documents
- Planning: [planning.md](./planning.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- Profile **batch-lite**: 소규모(단일 service + config 2종 + 문서), 인메모리만, 구조/인터페이스/DB 변경 없음, 설계가 dcy-010 §4 에 완비 → **Design Gate: no**(design.md 불요). implementation/verification 은 해당 stage 진입 시 생성.
- OQ-1(600s)/OQ-2(3, 경고만)/OQ-3(단일 per-slot) Answered. NFR-1(인메모리)/NFR-2(무락)/NFR-4(paper reject 시뮬 테스트) 는 REQ-052 AC 로 흡수.
- 발견 근거: REL-003 Feedback / 2026-06-07 실 KIS LIVE SL/TP e2e.
@@ -1,38 +0,0 @@
# BAT-014 Planning
- Status: confirmed
- Batch: BAT-014
- Profile: batch-lite
## Included REQ
- REQ-052 (Integration): `SafetySellService` per-slot 쿨다운(`_last_attempt`/`_attempt_count`) + `_safe_sell` 전 skip / 만료 재시도 + 경고 로그 + holding 이탈 정리. market_close 제외.
- REQ-053 (Configuration): `SafetySellConfig.min_retry_interval_sec`(env `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, 기본 600) + `max_retry_warn_count`(env `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, 기본 3).
- REQ-054 (Documentation): `docs/runtime-flows.md` safety-sell tick 쿨다운 단계.
## Delivery Plan
1. REQ-053: `SafetySellConfig` 필드 2종 + `from_env` 파싱 → `test_safety_sell_config.py` 기본값/파싱.
2. REQ-052: `SafetySellService.__init__` dict 2종 + `check_forced_close`/`check_prices` 쿨다운 gate(skip/update) + `attempt_count` 경고 + `run_once` holding 이탈 정리. SafetySellService 가 config 의 두 값을 사용. paper `reject_symbols` 시뮬 단위 테스트(skip/만료 재시도/경고/정리/market_close 제외/트리거 해소).
3. REQ-054: runtime-flows 갱신.
4. 전체 회귀 + ruff.
## Dependencies
- Internal: dcy-007/BAT-011 `SafetySellService`(변경 대상) + `SafetySellConfig`(REQ-041). paper `reject_symbols`(REQ-011/BAT-009) — 거부 시뮬.
- External: 없음.
## Design Gate
- Design Required Before Implementation: **no**
- Reason: 변경이 단일 service 인메모리 dict 추가 + config 2 필드 + 문서로 국소적이고, 구조/인터페이스/DB/스레드 영향이 없다(추가 스레드·스키마 없음, 단일 스레드 호출이라 무락). 쿨다운 알고리즘·기본값·트리거 적용 범위가 dcy-010 §4/§6(OQ-1/2/3 Answered)에 완비되어 설계 모호성이 없다 → batch-lite, design.md 생략.
## Milestones
1. M1 — REQ-053 config 2종 + 단위 green
2. M2 — REQ-052 쿨다운 로직 + paper reject 시뮬 단위 green
3. M3 — REQ-054 runtime-flows
4. M4 — 전체 회귀 + ruff
## Risks
- R-1 (Low): 쿨다운 중 트리거 해소 시 불필요 경고 — holding 이탈 정리(FR-4) + 조건 해소 시 `_safe_sell` 미호출로 완화.
- R-2 (Low): 재시작 시 쿨다운 초기화 → 1회 즉시 재시도 — 허용(문서 주의).
- R-3 (Low): paper 기본은 거부 안 함 → `reject_symbols` 시뮬로 테스트 커버.
## Notes
- batch-lite: design 생략(Design Gate no). implementation.md/verification.md 는 구현/검증 stage 진입 시 생성.
@@ -1,39 +0,0 @@
# BAT-014 Verification
- Status: verified
- Batch: BAT-014
## Acceptance Mapping
### REQ-052 (per-slot 쿨다운 + 경고)
| AC | Evidence |
|----|---------|
| AC-1: 쿨다운 이내 재시도 skip (S-1) | `test_safety_sell_worker.py::TestRetryCooldown::test_skips_retry_within_interval` (1회만) |
| AC-2: 쿨다운 만료 후 재시도 (S-2) | `test_retries_after_cooldown_expiry` (+601s → 2회) |
| AC-3: max_retry_warn_count 경고 (S-3) | `test_warning_after_max_count` (caplog WARNING + slot_id) |
| AC-4: holding 이탈 시 정리 (S-4) | `test_prune_on_non_holding` (`_last_attempt`/`_attempt_count` 제거) |
| AC-5: market_close 미적용 | `test_market_close_not_gated_by_cooldown` (쿨다운 중에도 청산) |
| AC-6: 트리거 해소 시 미증가 (R-1) | `test_trigger_resolved_no_increment` (sell 없음, count 없음) |
### REQ-053 (쿨다운 env 2종)
| AC | Evidence |
|----|---------|
| AC-1: 기본값 600/3 | `test_safety_sell_config.py::test_safety_sell_config_defaults` |
| AC-2: env 파싱(300/5) | `test_retry_cooldown_env_tuning` |
| AC-3: 미설정 회귀 없음 | 전체 278 passed (기존 safety-sell 동작 불변) |
### REQ-054 (runtime-flows 정합)
| AC | Evidence |
|----|---------|
| AC-1: 쿨다운 flow(skip/재시도/경고/정리) | `docs/runtime-flows.md` "safety-sell 재시도 쿨다운" flow |
| AC-2: market_close 제외 명시 | 동일 flow 첫 문단 + 장 마감 flow |
## Evidence
- Test: 신규 8종(쿨다운 6 + config 2) green. 전체 단위 **278 passed / 30 skipped**, `ruff` All passed.
- Manual: 없음(인메모리 로직 단위 커버; 영속 거부는 non-transitioning sell stub 로 시뮬 — dcy-010 NFR-4 의도).
- Logs: `34 passed`(대상 파일) / `278 passed`(전체).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 인메모리·additive(DB/스레드/인터페이스 변경 없음), default 미설정 시 기존 동작 불변. 실 운영 거부 시나리오 관찰은 차기 거래일 보강(비차단).
@@ -1,63 +0,0 @@
# BAT-015 Design
- Status: confirmed
- Batch: BAT-015
- Profile: standard
## 설계 목표
- buy intent 식별자 계약을 정식 식별자(portfolio_id=asset_id) 기준으로 hard cutover 하면서, symbol 입력 제거에 따른 다수 호출부 회귀를 통제한다.
- issue #11 의 ledger silent skip 을 무성 → 가시화로 전환하되 전이 동작과 outbox 스키마는 불변으로 유지한다(OQ-4).
## 1. buy admission 계약 (REQ-007)
### BuyIntent 구조
- `portfolio_id: str` 를 필수 위치 인자로 승격(intent_id, trading_date 다음). `symbol: Optional[str] = None` 으로 강등 — 클라이언트 입력이 아니라 admission 이 universe 에서 resolve 해 슬롯에 싣는 값.
- 나머지 필드(qty/allocated_capital/entry_price/strategy_id/thresholds/extra)는 불변.
### symbol resolve 주입 경계
- 결정: `IntentService(resolve_symbol=callable|None)` 콜백 주입(SnapshotStore 직접 의존 대신).
- 근거: (a) application 계층이 portfolio.SnapshotStore 에 직접 결합되지 않아 테스트 더블 주입이 쉽다. (b) `resolve_symbol=None` 폴백을 두면 symbol resolve 가 필요 없는 기존 단위 테스트는 portfolio_id 추가만으로 무회귀(R-1 통제). 프로덕션 daemon 은 항상 `_resolve_symbol`(SnapshotStore.get) 주입.
- `_resolve_symbol(portfolio_id) -> Optional[str]`. 부재 시 falsy.
### `_admit_buy` reject 분기 (FR-1/FR-2/FR-3)
1. `not intent.portfolio_id` → `reject(portfolio_id_required)` (FR-1, 근본 원인 제거).
2. resolver 주입됨: `symbol = resolve_symbol(portfolio_id)`; falsy 면 `reject(unknown_portfolio)` (FR-3, 단일 reject 정책 OQ-1 — symbol fallback 없음).
3. resolver 미주입 + `intent.symbol` 도 falsy → `reject(unknown_portfolio)` (호출 대상 미상).
4. resolve 성공 시 기존 idle-slot 배정·`transit_slot(IDLE→PENDING_BUY, symbol=symbol, portfolio_id=...)` 흐름 유지. extra 에도 resolved symbol 기록.
## 2. buy 메시지 payload 계약 (REQ-021)
- buy 핸들러는 payload 에서 `symbol` 을 읽지 않는다. `portfolio_id = _opt_str(body.get("portfolio_id")) or ""` 로 매핑(빈 문자열 → admission 의 portfolio_id_required 로 일관 거부).
- symbol 은 핸들러에서 BuyIntent 에 싣지 않음(서버 resolve 가 admission 책임).
## 3. daemon 배선 (REQ-007)
- `runtime/daemon.py::_resolve_symbol(self, portfolio_id)` → `asset.symbol if (asset := self.snapshot_store.get(portfolio_id)) else None`.
- `IntentService(..., resolve_symbol=self._resolve_symbol)` 주입.
## 4. ledger skip 가시화 (REQ-018, issue #11)
- `_record_ledger` 두 조기 반환 경로 모두에서 `r.ledger_skipped += 1` 후 `_log.warning(...)`:
- portfolio_id 누락: `reason=missing_portfolio_id` (slot_id/symbol/side).
- execution_id 누락: `reason=missing_execution_id` (slot_id/portfolio_id/side, 브로커 계약 회귀 방어 케이스).
- 슬롯 전이는 그대로(관측만 추가, OQ-4). outbox 마커 행/스키마 변경 없음.
- `execution.py` 에 `import logging` + `_log = logging.getLogger(__name__)` 추가.
## 5. 운영 점검 쿼리 (REQ-055, FR-5)
- `adapters/persistence/ops_checks.py::find_ledger_gap_slots(dsn, *, trading_date=None, psycopg_module=None)` — 읽기 전용.
- anti-join SQL: `daily_slot s WHERE s.status='closed' AND (td IS NULL OR trading_date=td) AND (portfolio_id IS NULL OR NOT EXISTS(SELECT 1 FROM ledger_outbox o WHERE o.asset_id=s.portfolio_id))`.
- 반환: `slot_id/trading_date/symbol/portfolio_id/broker_order_id` dict 리스트. `daily_slot` durable → outbox 마커 없이 영구 탐지(OQ-4 위임).
- 스키마 변경 없음. 알람/주기 실행 배선은 운영 절차(Out of Scope).
## 책임 분리
- 런타임 관측(REQ-018, WARN+카운터): "지금 skip 났다"를 즉시 노출.
- 사후 탐지(REQ-055, anti-join): durable 슬롯 기준으로 누락을 영구·집계 탐지.
- 둘 다 outbox 마커 행 없이 성립 → outbox 전송 큐 의미 오염 회피(OQ-4 근거).
## 하위호환 / cutover (NFR-3)
- 호환 창구 없음(OQ-2). symbol 기반 호출은 portfolio_id 누락 시 명확히 reject.
- 호출부 전환 전략:
- 비-핸들러 intent/동시성/repository 테스트: BuyIntent 에 portfolio_id 추가(resolver None → intent.symbol 폴백).
- 핸들러/RabbitMQ e2e 테스트: resolve_symbol 람다 + payload portfolio_id 동시 전환.
- ledger skip 테스트: admission 우회(`transit_slot(portfolio_id=None)`)로 skip 전제 재구성 + caplog 검증.
## 문서 (FR-6)
- `docs/interface-contract.md`: buy payload portfolio_id 필수 + symbol 제거/서버 resolve.
- `docs/runtime-flows.md`: buy admission step 에 portfolio_id 필수 + symbol resolve + unknown_portfolio + ledger skip 가시화 단락.
@@ -1,40 +0,0 @@
# BAT-015 Implementation
- Status: completed
- Batch: BAT-015
## 구현 요약
- dcy-011 buy intent 계약 hard cutover + ledger skip 가시화 + 운영 점검 쿼리를 단일 standard batch 로 구현. issue #6/#11 흡수.
## 변경 내역
### REQ-007 — buy admission portfolio_id 필수 + symbol resolve
- `trex_core/application/intents.py`:
- `BuyIntent`: `portfolio_id: str` 를 required 위치 인자로 승격, `symbol: Optional[str] = None` 로 강등(서버 resolve 결과를 담는 필드).
- `IntentService.__init__(..., resolve_symbol=None)` — `callable(portfolio_id) -> Optional[str]` 주입점.
- `_admit_buy`: ① `not portfolio_id` → `reject(portfolio_id_required)`; ② resolver 주입 시 `symbol = resolve_symbol(portfolio_id)`, falsy 면 `reject(unknown_portfolio)`; ③ resolver 미주입 + symbol falsy → `reject(unknown_portfolio)`; 이후 `transit_slot(symbol=symbol, portfolio_id=..., extra={..., "symbol": symbol})`.
- `trex_core/runtime/daemon.py`: `_resolve_symbol(self, portfolio_id)` → `asset.symbol if (asset := self.snapshot_store.get(portfolio_id)) else None`; `IntentService(..., resolve_symbol=self._resolve_symbol)` 배선.
### REQ-021 — buy 메시지 payload symbol 제거
- `trex_core/application/messaging_handlers.py`: buy 핸들러가 `symbol` 을 더 이상 읽지 않음; `portfolio_id=_opt_str(body.get("portfolio_id")) or ""` 매핑.
### REQ-018 — ledger skip 가시화 (issue #11)
- `trex_core/application/execution.py`: `import logging` + `_log = logging.getLogger(__name__)`; `_record_ledger` 두 skip 경로(portfolio_id 누락 `reason=missing_portfolio_id`, execution_id 누락 `reason=missing_execution_id`)에서 `r.ledger_skipped += 1` + `_log.warning(...)`. 슬롯 전이 동작 불변, outbox 스키마 무변경(OQ-4).
### REQ-055 — closed↔outbox anti-join 운영 점검 (신규)
- `trex_core/adapters/persistence/ops_checks.py`(신규): `find_ledger_gap_slots(dsn, *, trading_date=None, psycopg_module=None)` 읽기 전용. `status='closed'` 슬롯 중 `portfolio_id IS NULL` 또는 대응 `ledger_outbox` 행 부재(anti-join) 식별 → `slot_id/trading_date/symbol/portfolio_id/broker_order_id` dict 리스트. 스키마 변경 없음.
### FR-6 — 문서 정합
- `docs/interface-contract.md`: buy payload `portfolio_id` 필수 + `symbol` 제거(서버 resolve). Last Updated → BAT-015.
- `docs/runtime-flows.md`: buy admission step 에 portfolio_id 필수 + symbol resolve + unknown_portfolio + ledger skip 가시화 단락. Last Updated → BAT-015.
## 테스트 마이그레이션 (hard cutover, NFR-3)
- 비-핸들러(resolver None → intent.symbol 폴백, portfolio_id 추가만): `test_execution.py`, `test_intent_concurrency.py`, `test_messaging_snapshot_consumer.py`, `test_postgresql_repository.py`.
- 핸들러/e2e(resolver 람다 + payload portfolio_id): `test_messaging_intent_consumer.py`(+ 신규 3: without_portfolio_id_rejected / unknown_portfolio_rejected / symbol_resolved_from_universe), `test_rabbitmq_intent_e2e.py`.
- ledger skip(admission 우회 + caplog): `test_execution_ledger.py`(skip 테스트 재작성 + 신규 `test_ledger_skip_emits_warning`).
- 신규 PG 게이트: `tests/adapters/persistence/test_ops_checks.py`(gap 탐지 / 정상 0건).
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `282 passed / 32 skipped`(`::test_sandbox_token_and_submit` 는 LIVE `.env`(sandbox URL 아님)로 deselect — 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `314 passed`(PG anti-join 점검 + buy intent RabbitMQ e2e + 마이그레이션 round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
@@ -1,39 +0,0 @@
# BAT-015: buy intent portfolio_id 필수 계약 + symbol 서버 resolve + ledger skip 가시화 + 누락 점검 쿼리
- Status: released
- Profile: standard
- Source Discovery: dcy-011
- Included REQ: REQ-007 (CHG-20260608-01), REQ-018 (CHG-20260608-01), REQ-021 (CHG-20260608-01), REQ-055
## Summary
- dcy-011(buy intent — portfolio_id 필수 / symbol 서버 resolve + ledger silent skip 가시화)을 단일 standard batch 로 delivery 한다. issue #6(buy 식별자 정정)과 issue #11(portfolio_id 없는 체결의 ledger silent skip)을 한 주기로 흡수한다.
- buy intent 계약을 hard cutover: `portfolio_id` 필수화 + admission 시 `SnapshotStore` universe 에서 `symbol` resolve + universe 부재 시 `unknown_portfolio` 거부(REQ-007), buy 메시지 payload 에서 `symbol` 제거(REQ-021), `_record_ledger` skip 가시화(WARN 로그 + `ExecutionResult.ledger_skipped` 카운터, REQ-018), closed↔`ledger_outbox` anti-join 운영 점검 `find_ledger_gap_slots` 신규(REQ-055).
- 거버넌스: dcy-011 OQ-3 결정에 따라 기존 Implemented REQ(007/018/021)는 `change-req`, 기존 REQ 가 다루지 않는 신규 기능(FR-5 운영 점검 쿼리)만 신규 REQ-055.
## Scope
- In Scope:
- `BuyIntent` portfolio_id 필수화 + symbol Optional(서버 resolve), `IntentService(resolve_symbol=...)` 주입, `_admit_buy` portfolio_id_required/unknown_portfolio reject + symbol resolve (REQ-007)
- buy 메시지 핸들러 payload 에서 symbol 제거·portfolio_id 매핑 (REQ-021)
- `runtime/daemon.py` `_resolve_symbol`(SnapshotStore.get) 배선 (REQ-007)
- `_record_ledger` 두 skip 경로(portfolio_id/execution_id 누락) WARN 로그 + 카운터 가시화 (REQ-018)
- `adapters/persistence/ops_checks.py::find_ledger_gap_slots` 읽기 전용 anti-join 점검 (REQ-055)
- `docs/interface-contract.md` + `docs/runtime-flows.md` 정합 (FR-6, REQ-007/021/018 본문에 흡수)
- Out of Scope:
- sell intent 계약 변경(이미 portfolio_id/slot 기반)
- universe refresh 주기/스케줄 재설계
- symbol-only 레거시 호환 창구(OQ-2 hard cutover)
- `ledger_outbox` skip 마커 행 도입(OQ-4 — 로그+메트릭만, 영구 탐지는 REQ-055 anti-join 위임)
- 과거 누락분 데이터 복구(별도 운영 이슈)
- skip 점검 쿼리의 알람/주기 실행 배선(쿼리/함수가 산출물; 운영 배포 절차)
## 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-011). OQ-1~OQ-5 모두 Answered.
- hard cutover(NFR-3): symbol 기반 호출부(테스트 다수) 동시 전환. `IntentService(resolve_symbol=None)` 는 `intent.symbol` 로 폴백 → 비-핸들러 테스트 churn 최소화, 프로덕션 daemon 은 항상 SnapshotStore resolve.
- ledger skip 가시화(REQ-018)는 전이 동작 불변, 관측만 추가(OQ-4).
- REQ-055 anti-join 은 `daily_slot` durable 특성에 의존(outbox 마커 없이 영구 탐지).
@@ -1,53 +0,0 @@
# BAT-015 Planning
- Status: confirmed
- Batch: BAT-015
- Profile: standard
## Included REQ
- REQ-007 (Interface, change-req CHG-20260608-01): buy admission 계약 — `portfolio_id` 필수화, admission 시 `SnapshotStore` 에서 symbol resolve, universe 부재 시 `unknown_portfolio` 거부. symbol-필수/portfolio_id-optional 폐기(hard cutover).
- REQ-021 (Integration, change-req CHG-20260608-01): buy 메시지 payload 계약에서 `symbol` 제거, `portfolio_id` 필수.
- REQ-018 (Integration, change-req CHG-20260608-01): `_record_ledger` ledger skip 가시화(WARN 로그 + `ledger_skipped` 카운터, 두 skip 경로).
- REQ-055 (Exception, 신규): closed↔`ledger_outbox` anti-join 운영 점검 `find_ledger_gap_slots`(읽기 전용).
## Delivery Plan
1. REQ-007: `BuyIntent`(portfolio_id 필수 + symbol Optional 서버 resolve), `IntentService(resolve_symbol=...)`, `_admit_buy`(portfolio_id_required → resolve → unknown_portfolio). intent 단위 + 동시성 회귀.
2. REQ-021: buy 핸들러 payload 파싱(symbol 제거, portfolio_id 매핑). 핸들러 단위 + RabbitMQ intent e2e.
3. REQ-007 배선: `runtime/daemon.py::_resolve_symbol`(SnapshotStore.get) + IntentService 주입.
4. REQ-018: `_record_ledger` 두 skip 경로 WARN 로그 + 카운터. caplog 단위.
5. REQ-055: `adapters/persistence/ops_checks.py::find_ledger_gap_slots` anti-join SQL. PG 게이트 단위.
6. FR-6 문서: interface-contract / runtime-flows 정합.
7. 전체 회귀 + ruff + 인프라 게이트(PG + RabbitMQ).
## Dependencies
- Internal:
- dcy-002/BAT-002 intent 제출(REQ-007) — admission 변경 대상.
- dcy-006/BAT-007 messaging 핸들러(REQ-021) — buy payload 파싱 변경 대상.
- dcy-004/BAT-006 fill→ledger(REQ-018) — `_record_ledger` skip 가시화 대상.
- dcy-005/BAT-008 SnapshotStore(REQ-027/028) — portfolio_id→symbol resolve 소스.
- REQ-019 ledger outbox 스키마 — REQ-055 anti-join 대상(스키마 변경 없음).
- External:
- PostgreSQL(anti-join 점검). RabbitMQ(buy intent e2e).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) buy 계약 hard cutover 는 `BuyIntent` 구조 + admission + 메시지 핸들러 + daemon 배선 + 다수 테스트 표면에 동시 영향 — symbol 서버 resolve 의 주입 경계(`resolve_symbol` 콜백 vs SnapshotStore 직결)와 폴백 규칙을 설계에서 고정해야 회귀를 막는다. (b) reject 정책 분기(portfolio_id_required vs unknown_portfolio)와 universe 부재 판정 경로를 명확히 해야 admission 의미가 일관된다. (c) ledger skip 가시화(REQ-018)와 anti-join 점검(REQ-055)의 책임 분리(런타임 관측 vs 사후 탐지) + OQ-4 결정(outbox 마커 미도입)을 설계에서 못박아야 스키마 오염을 피한다. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-007 BuyIntent + IntentService resolve_symbol + _admit_buy reject/resolve, intent 단위 green
2. M2 — REQ-021 buy 핸들러 payload portfolio_id 기반, 핸들러 단위 + e2e green
3. M3 — REQ-007 daemon _resolve_symbol 배선
4. M4 — REQ-018 _record_ledger 가시화, caplog 단위 green
5. M5 — REQ-055 find_ledger_gap_slots, PG 게이트 단위 green
6. M6 — FR-6 문서 정합 + 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): hard cutover 가 symbol 기반 기존 테스트/호출부 다수를 깬다. 대응: `IntentService(resolve_symbol=None)` 는 `intent.symbol` 폴백 → 비-핸들러 테스트는 portfolio_id 추가만으로 무회귀; 핸들러/e2e 는 resolver + payload portfolio_id 동시 전환. 모든 호출부 식별·전환(NFR-3).
- R-2 (Medium): 매수 시점 universe 부재 시 정상 매수가 unknown_portfolio 로 거부. 대응: 단일 reject 정책(OQ-1) + 진단 reason/로그. 거래일 refresh 정합은 운영 점검(R-4와 연계).
- R-3 (Low, OQ-4): skip 마커 행 미도입 → 영구 탐지는 REQ-055 anti-join 으로 충족. outbox 스키마/dispatcher 무변경.
- R-4 (Medium, issue #11): 잔존/과거 누락 체결 탐지 지연. 대응: skip 가시화(REQ-018) + anti-join 점검(REQ-055). 과거분 복구는 범위 밖.
## Notes
- hard cutover 1차 게이트: 전체 회귀가 green 이어야 호출부 전환 누락이 없음을 보장.
- REQ-055 anti-join 은 PG 게이트(실 PostgreSQL) 통합으로 검증; gap 탐지 + 정상 0건.
- buy intent e2e(RabbitMQ)는 payload portfolio_id + resolver 경로 확인.
@@ -1,65 +0,0 @@
# BAT-015 Verification
- Status: verified
- Batch: BAT-015
## Acceptance Mapping
### REQ-007 (buy admission portfolio_id 필수 + symbol resolve, change-req)
| AC | Evidence |
|----|----------|
| AC-1 idle→pending_buy admit (portfolio_id 기반) | tests/application/test_messaging_intent_consumer.py::test_buy_symbol_resolved_from_universe + test_intent_concurrency.py::test_concurrent_buys_admit_exactly_idle_count |
| AC-2 sell 전이 (회귀 불변) | tests/application/test_execution_ledger.py (sell fill) + 전체 회귀 |
| AC-3 멱등 재전송 | test_intent_concurrency.py::test_duplicate_intent_id_admits_once_under_concurrency |
| AC-4 동시성 단일 writer | test_intent_concurrency.py::test_concurrent_buys_admit_exactly_idle_count |
| AC-5 결정 재조회 | 전체 회귀(IntentService.lookup) |
| portfolio_id_required 거부 (CHG delta) | test_messaging_intent_consumer.py::test_buy_without_portfolio_id_rejected |
| unknown_portfolio 거부 (CHG delta) | test_messaging_intent_consumer.py::test_buy_unknown_portfolio_rejected |
| symbol 서버 resolve (CHG delta) | test_messaging_intent_consumer.py::test_buy_symbol_resolved_from_universe |
### REQ-021 (buy payload symbol 제거, change-req)
| AC | Evidence |
|----|----------|
| buy payload portfolio_id 매핑(symbol 미사용) | test_messaging_intent_consumer.py (3 buy 테스트) |
| RabbitMQ buy intent e2e | tests/adapters/messaging/test_rabbitmq_intent_e2e.py (인프라 게이트) |
### REQ-018 (ledger skip 가시화, change-req)
| AC | Evidence |
|----|----------|
| AC-1 buy fill → 원장 1건 + holding | tests/application/test_execution_ledger.py::test_buy_fill_enqueues_one_ledger_item_and_writer_records_it |
| AC-2 sell fill → 원장 1건 + closed | test_execution_ledger.py::test_sell_fill_enqueues_sell_ledger_item |
| AC-3 FillStatus execution_id/fee | 위 buy 테스트 |
| AC-4 skip 시 전이 정상 + **가시화(WARN+카운터)** | test_execution_ledger.py::test_portfolio_id_absent_skips_ledger_but_transitions_slot + ::test_ledger_skip_emits_warning (caplog) |
| AC-5 단일 기록 주체 | test_domain_import_sweep + execution.py 단일 enqueue 지점 |
### REQ-055 (closed↔outbox anti-join 운영 점검, 신규)
| AC | Evidence |
|----|----------|
| AC-1 portfolio_id NULL closed 포함 (S-4) | tests/adapters/persistence/test_ops_checks.py::test_detects_only_gap_slots (PG 게이트) |
| AC-2 portfolio_id 있으나 outbox 부재 포함 | ::test_detects_only_gap_slots |
| AC-3 정상(outbox 일치) 제외 → 0건 | ::test_no_gaps_when_all_recorded |
| AC-4 비-closed 제외 | ::test_detects_only_gap_slots |
## Evidence
- Test:
- 단위 전체 회귀: `282 passed / 32 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` sandbox URL 아님, 본 batch 무관).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `314 passed`(PG anti-join 점검 gap 탐지/정상 0건 + buy intent RabbitMQ e2e + 마이그레이션 round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
- Logs: `282 passed, 32 skipped, 1 deselected` / `314 passed, 1 deselected ~15.5s`.
## 성공 기준(dcy-011) 대응
- S-1(portfolio_id 누락 거부 + portfolio_id 만으로 resolve admit): test_buy_without_portfolio_id_rejected + test_buy_symbol_resolved_from_universe.
- S-2(매수 슬롯 portfolio_id 귀속 + outbox/ledger 정합): buy fill 원장 테스트 + e2e.
- S-3(ledger skip 무성 금지, WARN 100%): test_ledger_skip_emits_warning(caplog).
- S-4(점검 쿼리 누락 정확 식별, 정상 0건): test_detects_only_gap_slots + test_no_gaps_when_all_recorded.
- S-5(코드-문서 정합): interface-contract / runtime-flows 갱신(FR-6).
## Carried-Over / 비차단
- 매수 시점 universe(SnapshotStore) portfolio_id 존재 보장은 운영 런타임 데이터(거래일 refresh 정합) — 부재는 `unknown_portfolio` reject 로 안전 처리(R-2).
- REQ-055 점검 쿼리의 알람/주기 실행 배선은 운영 배포 절차(Out of Scope) — 쿼리/함수가 산출물.
- 과거 누락분 데이터 복구는 본 주기 범위 밖(필요 시 별도 운영 이슈, R-4).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. hard cutover 호출부 전환 누락은 전체 회귀(314 passed) green 으로 부재 확인. LIVE sandbox 토큰 테스트 deselect 는 `.env` 환경 의존(본 batch 무관).
@@ -1,65 +0,0 @@
# BAT-016 Design
- Status: confirmed
- Batch: BAT-016
- Profile: standard
## 설계 목표
- 매도 주문이 매수 지정가(`entry_price`)로 나가는 버그(issue #10)를 도메인 모델 차원에서 구조적으로 제거한다.
- released 집행 경로 변경·컬럼 rename 마이그레이션을 회귀 없이 cutover 한다.
## 1. 슬롯 가격 필드 의미 통일 (REQ-002)
### rename
- `Slot.entry_price` → `Slot.limit_price` (side 무관 "현재 pending 주문 지정가"). `EXPECTED_SLOT_FIELDS` 동시 반영(컬럼 계약 가드가 곧 검출).
- `transit_slot`/`_apply_transition_fields` 의 `entry_price` 파라미터 → `limit_price`.
### limit_price 생애주기 (FR-1, 핵심)
- 결정: limit_price 는 **pending 상태에서만 유효**. `_apply_transition_fields` 의 generic 경로에서:
- `to_state ∈ {PENDING_BUY, PENDING_SELL}` → `limit_price = 입력값 if not None else slot.limit_price`(set on entry).
- `to_state ∈ {HOLDING, CLOSED}` → `limit_price = None`(clear on exit).
- `to_state == IDLE`(롤백/복구) → 기존 reset 분기에서 이미 None clear.
- 효과: PENDING_BUY→HOLDING 에서 매수 지정가가 clear 되므로, 이후 HOLDING→PENDING_SELL 에서 매도 가격 미전달이면 slot.limit_price 는 None → 시장가. 매수 지정가가 매도로 누수될 경로가 구조적으로 없음.
- 전이 매트릭스:
- IDLE→PENDING_BUY: set(매수 지정가) / PENDING_BUY→HOLDING: clear / HOLDING→PENDING_SELL: set(매도 지정가 또는 None) / PENDING_SELL→CLOSED: clear / *→IDLE: clear.
### SlotService.sell (REQ-036)
- `sell(slot_id, actor, reason, *, limit_price=None)`. default None → 자율 매도(safety-sell/kill-switch)는 가격 없이 호출 → 시장가. transit_slot 에 limit_price 전달.
## 2. dispatch 가격 결정 (REQ-013)
- `_dispatch`: side 분기 제거. `limit = int(slot.limit_price) if slot.limit_price else None`; `order_type = "00" if limit is not None else "01"`. 매수가 재사용 경로 삭제.
## 3. sell intent 계약 (REQ-007)
- `SellIntent.limit_price: Optional[float] = None`. `_admit_sell` → `transit_slot(..., limit_price=intent.limit_price)`.
- `_admit_buy`: 기존 `entry_price=intent.entry_price`(BuyIntent 외부 필드 유지) → 슬롯 `limit_price=intent.entry_price` 로 매핑(매수 동작 동일).
- 단일 메커니즘(가격 유무 분기, OQ-2): reason 별 코드 차등 없음.
## 4. 메시지 경계 (REQ-007, NFR-3)
- sell 핸들러: `limit_price=_opt_float(body.get("limit_price"))` 추가.
- buy 핸들러: 외부 payload 필드명 `entry_price` 유지(BuyIntent.entry_price).
- slot read(`_slot_to_dict`): 외부 read 키 `"entry_price"` 유지하되 내부 `s.limit_price` 로 매핑(계약 호환 — 외부 필드명 rename 은 본 주기 밖).
## 5. persistence + 마이그레이션 (REQ-006)
- `006_rename_entry_price_to_limit_price.sql`: `ALTER TABLE daily_slot RENAME COLUMN entry_price TO limit_price` 를 idempotent DO-block(컬럼 존재 && 신컬럼 부재 시에만)으로. 글롭 재적용 모델에서 안전. 순수 rename → 기존 행 데이터 보존(S-3).
- `postgresql.py`: `_SLOT_COLUMNS`/update_slot/_row_to_slot 의 entry_price → limit_price.
## 6. 컬럼 계약 가드 (REQ-003)
- `test_contract.py`(도메인 필드셋)·`test_migration_and_drift.py`(DB 컬럼 = EXPECTED_SLOT_FIELDS)는 상수 참조라 rename 자동 반영; PG 게이트에서 실 컬럼명 limit_price 검증.
## 7. baseline 문서 (REQ-015)
- runtime-flows: dispatch 가격 결정 = slot.limit_price 단일 필드 + limit_price 생애주기.
- interface-contract: sell payload optional limit_price + ORD_DVSN 매핑.
- data-model: 슬롯 limit_price 컬럼 의미/생애주기.
## 하위호환 / 회귀 통제
- 외부 buy/sell payload·slot read 필드명 유지(NFR-3) → 전략 모듈 계약 무파괴.
- sell limit_price optional default None → 기존 재량 매도 호출 시장가로 안전 동작.
- 전체 회귀 green 이 rename 호출부 누락 부재의 1차 게이트.
## 테스트 설계
- 신규 `tests/application/test_sell_pricing.py`(recording paper broker 로 OrderRequest 직접 검증):
- (a) 지정가 매수 슬롯 + 가격 미전달 매도 → sell order_type "01"/limit None(entry_price 미사용 — 핵심 버그 검출).
- (b) 지정가 매수 슬롯 + 가격 전달 매도 → "00"/그 가격.
- (c) SlotService.sell(가격 없음) → 시장가 + closed(S-2).
- (d) 시장가 매수 슬롯 매도 → 시장가(회귀 안전).
- `test_service.py`: PENDING_BUY→HOLDING 시 limit_price clear 단언 추가.
@@ -1,48 +0,0 @@
# BAT-016 Implementation
- Status: completed
- Batch: BAT-016
## 구현 요약
- dcy-012 매도 가격 버그(issue #10)를 슬롯 `entry_price`→`limit_price` rename + clear-on-exit 생애주기 + dispatch 단일 필드 결정으로 정정. 전부 change-req(신규 REQ 없음).
## 변경 내역
### REQ-002 — 슬롯 도메인 rename + 생애주기
- `trex_core/domain/slot/models.py`: `Slot.entry_price` → `limit_price`(주석: side 무관 pending 주문 지정가, clear-on-exit); `EXPECTED_SLOT_FIELDS` 의 entry_price → limit_price.
- `trex_core/domain/slot/service.py`:
- `transit_slot`/`_apply_transition_fields` 파라미터 `entry_price` → `limit_price`.
- IDLE reset 분기: `entry_price=None` → `limit_price=None`.
- generic 경로: `to_state ∈ {PENDING_BUY, PENDING_SELL}` 면 set(입력 or 유지), 아니면(HOLDING/CLOSED) `new_limit_price=None` clear.
- `SlotService.sell(..., *, limit_price=None)` 추가 → transit_slot 에 전달.
### REQ-013 — dispatch 단일 필드
- `trex_core/application/execution.py::_dispatch`: `limit = int(slot.limit_price) if slot.limit_price else None`(side 분기/entry_price 재사용 제거). order_type 은 limit 유무로 "00"/"01".
### REQ-007 — sell intent 가격
- `trex_core/application/intents.py`: `SellIntent.limit_price: Optional[float] = None`; `_admit_sell` → `transit_slot(..., limit_price=intent.limit_price)`; `_admit_buy` → `limit_price=intent.entry_price`(BuyIntent.entry_price 외부 필드 유지).
- `trex_core/application/messaging_handlers.py`: sell 핸들러 `limit_price=_opt_float(body.get("limit_price"))`; buy payload `entry_price` 유지; `_slot_to_dict` 외부 키 `"entry_price"` 유지하되 `s.limit_price` 매핑(NFR-3).
### REQ-036 — 자동 매도 시장가
- `SlotService.sell` optional `limit_price`(default None) → safety-sell/kill-switch 자율 매도는 가격 미전달 → 시장가.
### REQ-006 — 마이그레이션 + persistence
- `trex_core/adapters/persistence/sql/006_rename_entry_price_to_limit_price.sql`(신규): idempotent DO-block rename(컬럼 존재 && 신컬럼 부재 시에만). 순수 rename → 데이터 보존.
- `trex_core/adapters/persistence/postgresql.py`: `_SLOT_COLUMNS`/update_slot SQL+바인딩/_row_to_slot 의 entry_price → limit_price.
### REQ-003 — 컬럼 계약 가드
- `EXPECTED_SLOT_FIELDS` rename 으로 `test_contract.py`/`test_migration_and_drift.py` 자동 반영(상수 참조). PG 게이트에서 실 컬럼 limit_price 검증.
### REQ-015 — baseline 문서
- `docs/runtime-flows.md`: dispatch 가격 결정(slot.limit_price 단일 필드) + limit_price 생애주기 set/clear. Last Updated → BAT-016.
- `docs/interface-contract.md`: sell payload optional limit_price + ORD_DVSN 매핑. Last Updated → BAT-016.
- `docs/data-model.md`: 슬롯 limit_price 컬럼 의미/생애주기. Last Updated → BAT-016.
## 테스트
- 신규 `tests/application/test_sell_pricing.py`(recording paper broker): 4건 — (a) 지정가 매수+시장가 매도(entry_price 미사용), (b) 지정가 매수+지정가 매도, (c) SlotService.sell 시장가+closed, (d) 시장가 매수 매도 회귀.
- `tests/domain/slot/test_service.py`: PENDING_BUY→HOLDING 시 `limit_price is None` 단언 추가; 두 →HOLDING 전이의 obsolete `entry_price=` 인자 제거.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `286 passed / 32 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `318 passed`(006 rename 마이그레이션 적용 + 컬럼 계약 limit_price + 마이그레이션 idempotency 포함).
- `ruff check trex_core tests` → All checks passed.
@@ -1,36 +0,0 @@
# BAT-016: 매도 가격 정책 정정 + 슬롯 entry_price→limit_price rename (sell dispatch 버그)
- Status: released
- Profile: standard
- Source Discovery: dcy-012
- Included REQ: REQ-013 (CHG-20260608-01), REQ-007 (CHG-20260608-02), REQ-036 (CHG-20260608-01), REQ-002 (CHG-20260608-01), REQ-006 (CHG-20260608-01), REQ-003 (CHG-20260608-01), REQ-015 (CHG-20260608-01)
## Summary
- dcy-012(매도 주문이 현재가가 아닌 entry_price 지정가로 나가는 버그, issue #10)를 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-012 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**.
- 핵심: 슬롯의 매수 전용 `entry_price` 를 side 무관 "현재 pending 주문 지정가" 단일 필드 `limit_price` 로 내부 rename(Option A) 하고, pending 이탈(→HOLDING/→CLOSED) 시 clear 한다. `_dispatch` 는 `slot.limit_price` 한 필드만 읽어 가격/주문유형을 결정(값=지정가 `00`, 없음=시장가 `01`). `SellIntent`/sell payload 에 optional `limit_price` 추가, 자율 매도(safety-sell/kill-switch)는 가격 미전달 → 시장가.
## Scope
- In Scope:
- 슬롯 `entry_price` → `limit_price` 내부 rename(도메인 모델/EXPECTED_SLOT_FIELDS, REQ-002) + pending 이탈 clear 생애주기
- DB 컬럼 rename 마이그레이션 `006_*.sql`(idempotent) + persistence 매핑 (REQ-006)
- `_dispatch` 가 `slot.limit_price` 단일 필드로 가격/주문유형 결정 (REQ-013)
- `SellIntent`/sell payload optional `limit_price` + `_admit_sell` 운반, `SlotService.sell` optional 가격 (REQ-007/REQ-036)
- 컬럼 계약/드리프트 가드 갱신 (REQ-003)
- baseline 문서(runtime-flows/interface-contract/data-model) 정합 (REQ-015)
- Out of Scope:
- 외부 buy/sell payload·slot read 의 필드명(`entry_price`) rename — REQ-007/dcy-011 과 조율할 별도 계약 변경(NFR-3 호환 유지)
- issue #9(KIS 외부 취소 시 pending_sell 갇힘) 자체의 복구 로직 — 본 버그는 #9 의 한 원인 제거, 복구는 후속 분리
- dispatch 시 현재가 자동 조회(`get_current_price`) 기반 지정가 — OQ-1 미채택
- buy 주문 가격 의미 변경(매수 지정가는 동일 limit_price 로 set, 동작 동일)
## 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-012). OQ-1(하이브리드)·OQ-2(가격 유무 분기)·OQ-3(전부 change-req) Answered.
- 버그 재현 조건: `entry_price` 가 채워진 **지정가 매수 슬롯**의 매도. 시장가 매수 슬롯은 entry_price=NULL 로 우연히 정상 → 검증은 지정가 매수 슬롯 케이스 필수(S-1).
- `OrderRequest.limit_price`(broker_port) 는 기존 존재 — 슬롯 필드 rename 으로 이름까지 정합.
- 마이그레이션은 글롭 재적용 모델이므로 006 은 idempotent DO-block(컬럼 존재 시에만 rename).
@@ -1,55 +0,0 @@
# BAT-016 Planning
- Status: confirmed
- Batch: BAT-016
- Profile: standard
## Included REQ
- REQ-002 (Interface, change-req): Slot `entry_price`→`limit_price` rename + clear-on-exit(→HOLDING/→CLOSED) 생애주기.
- REQ-006 (Data, change-req): daily_slot 컬럼 rename 마이그레이션 006(idempotent) + persistence 매핑.
- REQ-013 (Integration, change-req): `_dispatch` 가 `slot.limit_price` 단일 필드로 가격/주문유형 결정(매수가 재사용 제거).
- REQ-007 (Interface, change-req): `SellIntent`/sell payload optional `limit_price` + `_admit_sell` 운반; buy entry_price→limit_price 매핑.
- REQ-036 (Interface, change-req): `SlotService.sell` optional `limit_price`(default None → 자율 매도 시장가).
- REQ-003 (Testing, change-req): 컬럼 계약/드리프트 가드 limit_price 반영.
- REQ-015 (Documentation, change-req): runtime-flows/interface-contract/data-model 정합.
## Delivery Plan
1. REQ-002: `models.py` Slot 필드 + EXPECTED_SLOT_FIELDS rename; `service.py` transit_slot/_apply_transition_fields 파라미터 rename + clear-on-exit(→HOLDING/→CLOSED), `SlotService.sell` optional limit_price.
2. REQ-013: `execution.py::_dispatch` 가 `slot.limit_price` 단일 필드 사용.
3. REQ-007: `intents.py` SellIntent.limit_price + _admit_sell 운반 + _admit_buy limit_price 매핑; `messaging_handlers.py` sell payload limit_price, slot read 외부 entry_price→limit_price 매핑.
4. REQ-006: `006_*.sql` rename 마이그레이션(idempotent) + `postgresql.py` 매핑.
5. REQ-003: 컬럼 계약/드리프트 가드 확인(상수 참조 자동 + DB 게이트).
6. REQ-015: baseline 3문서 정합.
7. 신규 테스트(test_sell_pricing) + 전체 회귀 + ruff + 인프라 게이트(PG 마이그레이션/컬럼 계약).
## Dependencies
- Internal:
- dcy-003/BAT-004 broker execution(`_dispatch`) — 버그 지점.
- dcy-001/BAT-001 slot 도메인(REQ-002/003) — rename 대상.
- dcy-002/BAT-002 persistence(REQ-006) — 컬럼/마이그레이션.
- dcy-007/BAT-010 safety-sell(REQ-036 SlotService.sell) — 자율 매도 시장가 경로.
- dcy-011/BAT-015 — slot.symbol 서버 resolve(가정 A-1) 충족.
- External:
- PostgreSQL(rename 마이그레이션). KIS ORD_DVSN 00/01 매핑(OrderRequest 경유).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) `entry_price→limit_price` rename 은 도메인 모델·서비스 전이·persistence·마이그레이션·컬럼 계약 가드까지 **교차 절단(cross-cutting)** — 전이별 set/clear 매트릭스를 설계에서 못박지 않으면 매수 지정가 누수/복원 회귀(R-4) 위험. (b) 매도 가격 하이브리드(가격 유무 분기)의 단일 메커니즘 경계(자율=None=시장가, 재량=optional 지정가)를 설계에서 고정해야 reason 별 분기 혼입을 막는다(OQ-2). (c) released 집행 경로(R-1)·컬럼 rename 마이그레이션 idempotency 를 설계에서 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-002 도메인 rename + clear-on-exit, 단위 green
2. M2 — REQ-013 _dispatch limit_price 단일 필드 + REQ-007/036 sell 가격 경로, test_sell_pricing green
3. M3 — REQ-006 마이그레이션 006 + persistence 매핑, PG 게이트 컬럼 계약 green
4. M4 — REQ-003 가드 + REQ-015 문서 정합
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): released 집행 경로 매도 동작 변경. 대응: paper recording broker 로 손절/지정가/현재가>매수가 시나리오 검증 + change-req 이력. 실거래 배포는 release 단계 검증.
- R-2 (Medium): 재량 지정가 미체결 시 pending_sell 잔류. 대응: 본 주기는 가격 결정 분기에 한정, 미체결 처리는 issue #9/기존 롤백 범위.
- R-3 (Low, OQ-2): reason 별 차등 없이 가격 유무로 분기 → 자율 매도는 default None 으로 항상 시장가(체결성 보장).
- R-4 (Medium): rename/clear-on-exit 가 도메인/persistence/마이그레이션 교차. 대응: 전이별 set/clear 단위 + 컬럼 계약 가드 + idempotent 마이그레이션 + 외부 필드명 유지(경계 매핑).
## Notes
- 1차 게이트: 전체 회귀 green(rename 호출부 누락 부재 확인).
- 버그 검증은 반드시 지정가 매수 슬롯(entry_price 채워짐) 매도 케이스 포함(S-1 측정 조건).
- 마이그레이션은 PG 게이트(실 PostgreSQL) 컬럼 계약/드리프트 테스트로 검증.
@@ -1,74 +0,0 @@
# BAT-016 Verification
- Status: verified
- Batch: BAT-016
## Acceptance Mapping
### REQ-013 (dispatch 단일 필드, change-req)
| AC | Evidence |
|----|----------|
| dispatch 가 slot.limit_price 단일 필드로 결정(매수가 재사용 금지) | tests/application/test_sell_pricing.py::test_limit_buy_then_market_sell_does_not_reuse_buy_limit |
| 기존 buy/sell lifecycle 회귀 | tests/application/test_execution.py (전체) |
### REQ-007 (sell intent 가격, change-req)
| AC | Evidence |
|----|----------|
| sell optional limit_price → 지정가 | test_sell_pricing.py::test_limit_buy_then_limit_sell_uses_intent_price |
| 미지정 → 시장가(호환) | test_sell_pricing.py::test_limit_buy_then_market_sell_does_not_reuse_buy_limit |
| buy entry_price → 슬롯 limit_price 매핑(동작 동일) | test_sell_pricing.py::test_limit_buy_then_market_sell... (buy_req order_type "00"/1000) |
| 멱등/동시성 회귀 | tests/application/test_intent_concurrency.py |
### REQ-036 (자동 매도 시장가, change-req)
| AC | Evidence |
|----|----------|
| SlotService.sell(가격 없음) → 시장가 + closed | test_sell_pricing.py::test_safety_sell_dispatches_at_market |
| idempotency 회귀 | tests/application/test_auto_sell* / 전체 회귀 |
### REQ-002 (도메인 rename + clear-on-exit, change-req)
| AC | Evidence |
|----|----------|
| limit_price clear on →HOLDING | tests/domain/slot/test_service.py::test_pending_buy_to_holding (limit_price is None) |
| clear on →CLOSED / →IDLE | test_service.py::test_closed_to_idle_resets_fields_and_bumps_counter + 전체 회귀 |
| 필드셋 계약 | tests/domain/slot/test_contract.py |
### REQ-006 (마이그레이션 + persistence, change-req)
| AC | Evidence |
|----|----------|
| 컬럼 rename(데이터 보존) | tests/adapters/persistence/test_migration_and_drift.py::test_daily_slot_columns_match_expected_contract (PG 게이트) |
| 마이그레이션 idempotent | ::test_apply_migrations_is_idempotent |
| persistence round-trip | PG 게이트 repository/전이 테스트 전반 |
### REQ-003 (컬럼 계약 가드, change-req)
| AC | Evidence |
|----|----------|
| 도메인 필드셋 = limit_price | tests/domain/slot/test_contract.py |
| DB 컬럼 드리프트 가드 = limit_price | test_migration_and_drift.py::test_daily_slot_columns_match_expected_contract / ::test_drift_detected_when_contract_extended |
### REQ-015 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| runtime-flows/interface-contract/data-model 정합 | 문서 diff(BAT-016) |
## Evidence
- Test:
- 단위 전체 회귀: `286 passed / 32 skipped`(신규 sell-pricing 4 + holding clear 단언 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `318 passed`(006 rename 마이그레이션 + 컬럼 계약 limit_price + idempotency + 전이 round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
- Logs: `286 passed, 32 skipped, 1 deselected` / `318 passed, 1 deselected ~15.8s`.
## 성공 기준(dcy-012) 대응
- S-1(매도가 매수 지정가 재사용 안 함): test_limit_buy_then_market_sell_does_not_reuse_buy_limit(a) + test_limit_buy_then_limit_sell_uses_intent_price(b) + test_market_buy_slot_sells_at_market_regression(c). **지정가 매수 슬롯** 케이스 포함(측정 필수 조건 충족).
- S-2(손절 시장가 체결, pending_sell 갇힘 없음): test_safety_sell_dispatches_at_market.
- S-3(rename 마이그레이션 데이터 보존 + 생애주기): test_migration_and_drift(컬럼 계약+idempotency) + test_service(holding clear).
- S-4(코드-문서 정합): runtime-flows/interface-contract/data-model 갱신.
## Carried-Over / 비차단
- KIS 시장가 매도(`ORD_DVSN="01"`)의 paper/실계좌 수용·체결 특성(A-4): paper 검증 완료, 실계좌는 release 단계 운영 검증.
- 재량 지정가 미체결 시 pending_sell 잔류(R-2)·issue #9 복구는 본 주기 밖.
- 과거 누락/잘못 체결 데이터 보정은 범위 밖.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. rename 호출부 전환 누락은 전체 회귀(318 passed) green 으로 부재 확인. 외부 payload/read 필드명 유지로 전략 모듈 계약 무파괴. LIVE sandbox 토큰 테스트 deselect 는 `.env` 환경 의존(본 batch 무관).
@@ -1,49 +0,0 @@
# BAT-017 Design
- Status: confirmed
- Batch: BAT-017
## 설계 목표
- KIS 외부 취소를 종료 상태로 정확히 매핑해 pending_sell 영구 갇힘(issue #9)을 자동 복구한다.
- 부분 체결 후 취소를 정확 회계(과매도·이중기록 방지)하고, 미지 원인 갇힘을 Alert-only 워치독으로 가시화한다.
## 1. FillStatus 계약 확장 (REQ-012)
- `FillStatus.cancelled: bool = False` 추가. `is_partial_cancel` 프로퍼티 = `cancelled and filled_qty > 0`.
- 매핑 규칙:
- 체결 0 + 외부 취소 → `state="cancelled"`(기존 `is_terminal_reject` True).
- 부분 체결(0<ccld<ord) + 외부 취소 → `state="partially_filled"` + `cancelled=True`(is_terminal_reject False, is_partial_cancel True) + execution_id/fee/avg_price 운반.
- 전량 체결 → `state="filled"`(취소 플래그 무시).
## 2. KIS poll_fill (REQ-012)
- `inquire-daily-ccld` row 의 `cncl_yn` 을 파싱: `str(cncl_yn).strip().upper() == "Y"`. 미지/부재 → 비취소(보수적, R-1; stale 워치독이 backstop).
- ccld_qty<=0: cancelled 면 `cancelled` 반환, 아니면 기존 `accepted`.
- 0<ccld<ord: cancelled 면 execution_id(`kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{ccld_qty}`)+fee 합성 후 partial+cancelled 반환, 아니면 기존 partially_filled.
- 전량 체결: 기존 filled 경로 불변.
## 3. 집행 처리 (REQ-013/018, OQ-1 A안)
- `_poll` 분기 순서: `is_filled` → `is_partial_cancel` → `is_terminal_reject` → else(대기+stale 체크).
- 전량 취소(`cancelled`, ccld 0): 기존 `_rollback` 재사용 — 매도 `pending_sell→holding`+clear_dispatch, 매수 `pending_buy→idle`. holding 진입 시 limit_price clear(dcy-012) → 재매도 시장가(A-1).
- 부분 체결 후 취소 `_on_partial_cancel`:
- 매도(PENDING_SELL): `remaining = slot.qty - filled`; `transit_slot(PENDING_SELL→HOLDING, qty=remaining)`(avg_price=None → cost basis 유지). 체결분(filled, 체결가) 원장 1회(`_record_ledger` 재사용).
- 매수(PENDING_BUY): `transit_slot(PENDING_BUY→HOLDING, qty=filled, avg_price=fill)`. 체결분 원장 1회.
- 공통: clear_dispatch + event emit. `r.filled += 1`, `_record_ledger` 가 `r.ledger_enqueued += 1`.
- 멱등: `SlotStateMismatchError` 시 조용히 반환(다음 tick reconcile).
## 4. pending stale 워치독 (REQ-014, OQ-2 Alert-only)
- ExecutionService `stale_pending_threshold_sec`(<=0/None=비활성). `_check_stale_pending(slot, now, r)`: `age = now - slot.updated_at`; `age >= threshold` 면 `r.stale_pending += 1` + WARN. **상태 불변**.
- `slot.updated_at` 출처: mark_dispatched/transit 가 갱신 → "마지막 쓰기 이후 경과"가 곧 "pending 체류 시간" 근사.
- config: `DaemonConfig.pending_stale_threshold_sec`(env `TREX_PENDING_STALE_THRESHOLD`, default 0). daemon 이 ExecutionService 에 주입.
## 5. 포지션 정합 연계 (REQ-031)
- `_on_partial_cancel` 이 slot.qty 를 잔여 held 와 일치시켜 reconcile_positions 의 입력 정합을 유지. reconcile 코드 자체 변경 없음(연계 change-req).
## 6. baseline 문서 (REQ-015)
- interface-contract: FillStatus.cancelled/is_partial_cancel + state별 집행 처리 + KIS cncl_yn 매핑.
- runtime-flows: poll 단계 외부 취소(전량→롤백 / 부분→회계) + Alert-only stale 워치독 + limit_price clear 재매도.
## 자동 복구 성립 근거 (A-2)
- `is_terminal_reject` 가 이미 `cancelled` 포함 + `_rollback` 이 pending_sell→holding 수행 → KIS 어댑터가 cancelled 만 정확히 반환하면 application 경로 재사용으로 복구(전량 취소 케이스는 코드 추가 없이 성립).
## 테스트 설계
- `tests/adapters/broker/test_kis_cancel.py`(httpx stub): full cancel→cancelled, zero-fill non-cancel→accepted, partial+cancel→is_partial_cancel(execution_id 확인), full fill 회귀.
- `tests/application/test_execution_cancel.py`(programmable broker): 전량 취소→holding 복귀+clear, 부분 취소→원장 1건+qty 차감+cost basis 유지, stale 워치독 WARN(상태 불변), 비활성 default.
@@ -1,39 +0,0 @@
# BAT-017 Implementation
- Status: completed
- Batch: BAT-017
## 구현 요약
- dcy-013 KIS 외부 취소 감지(issue #9) + 부분 체결 회계 + pending stale 워치독(Alert-only)을 전부 change-req 로 구현(신규 REQ 없음).
## 변경 내역
### REQ-012 — KIS 취소 감지 + FillStatus 확장
- `trex_core/application/broker_port.py`: `FillStatus.cancelled: bool = False` + `is_partial_cancel`(=`cancelled and filled_qty > 0`).
- `trex_core/adapters/broker/kis.py::poll_fill`: `cncl_yn` 파싱(`=="Y"`, 미지 값 보수적 비취소). 체결 0+취소 → `state="cancelled"`; 부분 체결+취소 → `state="partially_filled"` + `cancelled=True` + execution_id/fee 합성. 전량 체결 경로 불변.
### REQ-013/018 — 집행 부분 체결 회계
- `trex_core/application/execution.py::_poll`: `is_filled` → `is_partial_cancel` → `is_terminal_reject` → else(대기+stale) 분기.
- `_on_partial_cancel`: 매도 `PENDING_SELL→HOLDING qty=잔여`(cost basis 유지), 매수 `PENDING_BUY→HOLDING qty=체결분 avg_price=체결가`; `_record_ledger`(REQ-018 재사용)로 체결분 원장 1회 + clear_dispatch + event. `r.filled += 1`.
- 전량 취소(`cancelled`, 체결 0)는 기존 `is_terminal_reject` → `_rollback`(holding 복귀 + clear_dispatch) 재사용.
### REQ-014 — pending stale 워치독 (Alert-only)
- `execution.py`: `ExecutionService(stale_pending_threshold_sec=...)`(<=0/None 비활성), `_check_stale_pending`(age=now-updated_at, 초과 시 WARN + `r.stale_pending += 1`, 상태 불변), `ExecutionResult.stale_pending`.
- `trex_core/runtime/config.py`: `DaemonConfig.pending_stale_threshold_sec`(env `TREX_PENDING_STALE_THRESHOLD`, default 0).
- `trex_core/runtime/daemon.py`: ExecutionService 에 threshold 주입.
### REQ-031 — 포지션 정합 연계
- `_on_partial_cancel` 이 slot.qty 를 잔여 held 와 일치시켜 정합 입력 유지(과매도 방지). reconcile_positions 코드 변경 없음.
### REQ-015 — baseline 문서
- `docs/interface-contract.md`: FillStatus.cancelled/is_partial_cancel + state별 집행 처리 + KIS cncl_yn 매핑. Last Updated → BAT-017.
- `docs/runtime-flows.md`: poll 단계 외부 취소(전량→롤백 / 부분→회계) + Alert-only stale 워치독. Last Updated → BAT-017.
## 테스트
- 신규 `tests/adapters/broker/test_kis_cancel.py`(httpx stub): 4건 — full cancel→cancelled, zero-fill non-cancel→accepted, partial+cancel→is_partial_cancel(execution_id), full fill 회귀.
- 신규 `tests/application/test_execution_cancel.py`(programmable broker): 4건 — 전량 취소→holding 복귀+clear, 부분 취소→sell 원장 1건+qty 6 잔여+cost basis 1000 유지, stale 워치독 WARN(상태 PENDING_SELL 불변), 비활성 default 무경고.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `294 passed / 32 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `326 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -1,36 +0,0 @@
# BAT-017: KIS 외부 취소 감지 + 부분 체결 회계 + pending stale 워치독 (issue #9)
- Status: released
- Profile: standard
- Source Discovery: dcy-013
- Included REQ: REQ-012 (CHG-20260608-01), REQ-013 (CHG-20260608-02), REQ-018 (CHG-20260608-02), REQ-031 (CHG-20260608-01), REQ-014 (CHG-20260608-01), REQ-015 (CHG-20260608-02)
## Summary
- dcy-013(KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구 갇힘 — cncl_yn 미파싱, issue #9)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-013 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**(워치독 임계 env 는 REQ-014 에 흡수).
- 핵심: KIS `poll_fill` 이 `inquire-daily-ccld` 의 `cncl_yn="Y"` 를 파싱해 외부 취소를 감지한다. 체결 0 취소 → `cancelled`(기존 롤백 경로로 holding 자동 복구), 부분 체결 후 취소 → `is_partial_cancel`(체결분 원장 1회 + qty 차감 + 잔여 holding 복귀, OQ-1 A안). 보강책으로 pending 주문 stale 워치독(OQ-2 Alert-only: WARN+카운터, 상태 미변경).
## Scope
- In Scope:
- KIS `poll_fill` 의 `cncl_yn` 파싱 + `cancelled`/partial-cancel 매핑, `FillStatus.cancelled`/`is_partial_cancel` (REQ-012)
- 집행 루프 부분 체결 회계 `_on_partial_cancel`(체결분 원장 + qty 차감 + holding 복귀), 전량 취소 롤백 재사용 (REQ-013)
- 부분 체결분 원장 1회 기록(`_record_ledger` 재사용) (REQ-018)
- 잔여 포지션 정합 연계(슬롯 qty ↔ 실 잔고, 과매도 방지) (REQ-031)
- pending stale 워치독 Alert-only + `TREX_PENDING_STALE_THRESHOLD` env (REQ-014)
- baseline 문서(interface-contract/runtime-flows) 정합 (REQ-015)
- Out of Scope:
- buy 경로·paper 브로커 동작 변경(paper 는 이미 취소 정상 처리)
- 매도 가격 정책(dcy-012/BAT-016 에서 처리 — 본 주기는 취소 *감지*만)
- 워치독 auto-remediation(타임아웃 시 자동 롤백/취소) — 정상 slow fill·지정가 대기 상호작용·KIS 취소 능력 필요로 후속 분리
- 과거 갇힘 슬롯의 일괄 보정(운영 절차)
## 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-013). OQ-1(A안 정확 회계)·OQ-2(워치독 Alert-only)·OQ-3(전부 change-req) Answered.
- 자동 회복 성립 근거: `FillStatus.is_terminal_reject` 가 이미 `cancelled` 를 포함하고 `_rollback` 이 `pending_sell→holding` 을 수행(A-2) → KIS 어댑터가 취소만 정확히 반환하면 application 경로 재사용으로 복구.
- dcy-012(BAT-016) 가정 충족: holding 진입 시 `limit_price` clear → 재매도 시장가(A-1).
- R-1(KIS 취소 필드 규약): `cncl_yn` 가정. 실 응답 확인은 운영/검증 단계 — 미지 값 보수 처리 + stale 워치독(FR-3) backstop.
@@ -1,51 +0,0 @@
# BAT-017 Planning
- Status: confirmed
- Batch: BAT-017
- Profile: standard
## Included REQ
- REQ-012 (Integration, change-req): KIS `poll_fill` `cncl_yn` 파싱 + `cancelled`/partial-cancel 매핑, `FillStatus.cancelled`/`is_partial_cancel`.
- REQ-013 (Integration, change-req): 집행 루프 `_on_partial_cancel`(체결분 원장 + qty 차감 + holding 복귀), 전량 취소 롤백 재사용.
- REQ-018 (Integration, change-req): 부분 체결분 원장 1회 기록(`_record_ledger` 재사용).
- REQ-031 (Exception, change-req): 잔여 포지션 정합 연계(슬롯 qty ↔ 실 잔고).
- REQ-014 (Exception, change-req): pending stale 워치독 Alert-only + `TREX_PENDING_STALE_THRESHOLD`.
- REQ-015 (Documentation, change-req): interface-contract/runtime-flows 정합.
## Delivery Plan
1. REQ-012: `broker_port.py` FillStatus.cancelled + is_partial_cancel; `kis.py::poll_fill` cncl_yn 파싱(full cancel/partial cancel 매핑 + execution_id).
2. REQ-013/018: `execution.py::_poll` is_partial_cancel 분기 + `_on_partial_cancel`(원장 1회 + qty 차감 + holding). 전량 취소는 기존 is_terminal_reject 롤백.
3. REQ-014: `_check_stale_pending` + ExecutionResult.stale_pending + ExecutionService threshold; `config.py` `pending_stale_threshold_sec`/env; `daemon.py` 배선.
4. REQ-031: 부분 회계가 잔여 qty 정합 유지(연계, reconcile 코드 무변경) 확인.
5. REQ-015: interface-contract/runtime-flows 정합.
6. 신규 테스트(test_kis_cancel, test_execution_cancel) + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-003/BAT-004 broker execution(poll/rollback/REQ-012/013/014) — 변경 대상.
- dcy-004/BAT-006 fill→ledger(REQ-018) — 부분 체결분 기록.
- dcy-005/BAT-008 reconcile_positions(REQ-031) — 잔여 정합 연계.
- dcy-012/BAT-016 — holding 진입 시 limit_price clear(A-1, 재매도 시장가).
- External:
- KIS `inquire-daily-ccld` cncl_yn 필드(R-1, 실 응답 확인은 운영/검증).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) 부분 체결 후 취소 회계(A안)는 FillStatus 계약 확장 + 집행 분기 + 원장 기록 + 슬롯 qty/cost basis 정합까지 교차 — 매도/매수 각각의 qty·avg_price 처리 규칙을 설계에서 못박지 않으면 과매도/이중기록(R-3) 위험. (b) 전량 취소 자동 복구가 기존 is_terminal_reject 롤백 재사용으로 성립하는지(A-2) 설계에서 확인. (c) stale 워치독은 Alert-only 경계(상태 미변경)·임계 env·타임스탬프 출처(slot.updated_at)를 설계에서 고정. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-012 FillStatus.cancelled + KIS cncl_yn 매핑, test_kis_cancel green
2. M2 — REQ-013/018 부분 체결 회계 + 전량 취소 롤백, test_execution_cancel green
3. M3 — REQ-014 stale 워치독 + config/daemon 배선
4. M4 — REQ-031 잔여 정합 확인 + REQ-015 문서 정합
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): KIS 실제 취소 표현(cncl_yn 필드/값)이 가정과 다르면 취소를 여전히 accepted 로 오판. 대응: 미지 값 보수 처리 + stale 워치독(FR-3) backstop. 실 응답 확인은 운영/검증 단계.
- R-2 (Medium): 외부 취소→롤백→재매도 반복 시 safety-sell 무한 재매도. 대응: dcy-010(REQ-052) per-slot 쿨다운 확인, 필요 시 cancelled 도 쿨다운 트리거 포함(후속).
- R-3 (Medium): 부분 체결 후 단순 롤백 시 과매도. 대응: A안(체결분 원장 + qty 차감 + 잔여 holding)으로 구조적 차단, cost basis 유지.
## Notes
- 1차 게이트: 전체 회귀 green.
- KIS 어댑터 단위는 httpx stub(fixture)로 cncl_yn 매핑 검증; 집행 회계는 programmable broker 로 FillStatus 주입 검증.
- 실 KIS 외부 취소 e2e 는 실계좌 세션 필요로 비차단 이월(운영 검증).
@@ -1,67 +0,0 @@
# BAT-017 Verification
- Status: verified
- Batch: BAT-017
## Acceptance Mapping
### REQ-012 (KIS 취소 감지, change-req)
| AC | Evidence |
|----|----------|
| cncl_yn → cancelled (체결 0) | tests/adapters/broker/test_kis_cancel.py::test_full_external_cancel_maps_to_cancelled |
| 비취소 zero-fill → accepted | ::test_zero_fill_not_cancelled_stays_accepted |
| 부분 체결 + 취소 → is_partial_cancel + execution_id | ::test_partial_fill_then_cancel_flags_partial_cancel |
| 전량 체결 회귀 | ::test_full_fill_unaffected_by_cancel_flag |
### REQ-013 (집행 부분 체결 회계, change-req)
| AC | Evidence |
|----|----------|
| 전량 외부 취소 → holding 복귀 + clear (영구 갇힘 0) | tests/application/test_execution_cancel.py::test_full_external_cancel_returns_slot_to_holding |
| 부분 취소 → qty 차감 + holding 잔여 + cost basis 유지 | ::test_partial_fill_then_cancel_records_and_keeps_remainder |
| 기존 filled/reject 회귀 | tests/application/test_execution.py 전체 |
### REQ-018 (부분 체결분 원장 1회, change-req)
| AC | Evidence |
|----|----------|
| 부분 취소 시 sell 원장 1건(qty=체결분, price=체결가) | test_execution_cancel.py::test_partial_fill_then_cancel_records_and_keeps_remainder |
### REQ-031 (포지션 정합 연계, change-req)
| AC | Evidence |
|----|----------|
| 부분 취소 후 slot.qty = 잔여 held (과매도 0) | test_execution_cancel.py::test_partial_fill_then_cancel_records_and_keeps_remainder (qty==6) |
| reconcile 로직 불변 회귀 | tests/application/test_reconciliation.py 전체 |
### REQ-014 (pending stale 워치독 Alert-only, change-req)
| AC | Evidence |
|----|----------|
| 임계 초과 pending → WARN + 카운터, 상태 미변경 | test_execution_cancel.py::test_stale_pending_watchdog_warns_without_state_change |
| 비활성(default) → 무경고 | ::test_stale_watchdog_disabled_by_default |
### REQ-015 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| interface-contract/runtime-flows 정합 | 문서 diff(BAT-017) |
## Evidence
- Test:
- 단위 전체 회귀: `294 passed / 32 skipped`(신규 KIS 취소 4 + 집행 취소/워치독 4 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `326 passed`.
- `ruff check trex_core tests` → All checks passed.
- Logs: `294 passed, 32 skipped, 1 deselected` / `326 passed, 1 deselected ~15.9s`.
## 성공 기준(dcy-013) 대응
- S-1(외부 취소 → cancelled 감지 → holding 복귀, 영구 갇힘 0): test_full_external_cancel_maps_to_cancelled + test_full_external_cancel_returns_slot_to_holding.
- S-2(죽은 주문 추가 폴링 없음): 취소 감지 후 clear_dispatch 로 broker_order_id 비워져 list_dispatched_pending 에서 제외(폴링 중단) — test_full_external_cancel_returns_slot_to_holding(broker_order_id is None).
- S-3(부분 체결분 원장 1회 + 잔여 정합, 과매도/이중기록 0): test_partial_fill_then_cancel_records_and_keeps_remainder(sell 1건 + qty 6 + cost basis 1000).
- S-4(임계 초과 pending stale 가시화, 상태 미변경): test_stale_pending_watchdog_warns_without_state_change.
## Carried-Over / 비차단
- R-1(KIS `cncl_yn` 실제 필드/값 규약): `cncl_yn="Y"` 가정으로 구현, 실 응답 확인은 운영/검증 단계. 미지 값 보수 처리 + stale 워치독(FR-3) backstop 으로 무한 갇힘 가시화 안전망 확보.
- 실 KIS 외부 취소 full-stack e2e: 실계좌 세션 필요로 이월(운영 검증, 비차단).
- R-2(외부 취소→재매도 반복 루프): dcy-010 per-slot 쿨다운과 연계, cancelled 쿨다운 트리거 포함은 후속.
- 워치독 auto-remediation: 후속 분리(OQ-2 Alert-only 결정).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 전량 취소 자동 복구·부분 체결 회계·stale 워치독 모두 단위 green(326 passed 회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관). KIS cncl_yn 실 규약은 운영 검증 비차단(보수 처리 + 워치독 backstop).
@@ -1,42 +0,0 @@
# BAT-018 Design
- Status: confirmed
- Batch: BAT-018
## 설계 목표
- `trading_date` 를 설정 타임존(기본 KST) 기준으로 분리해 KST 캘린더 일자와 DB 일자를 일치시키고, 재시작 없이 일자 경계마다 다음날 슬롯을 자동 생성한다(issue #14). timestamp 는 UTC 유지(NFR-1).
## 1. DailyPreallocationScheduler (REQ-005, REQ-030 패턴)
- `application/preallocation_scheduler.py`(신규). 의존: `slot_service`(preallocate_daily_slots), `total_slots`, `tz`(tzinfo), `schedule_time`(time, 기본 00:00), `clock`(주입식, 기본 utcnow).
- `run_once()`: `trading_date = clock().astimezone(tz).date()` → `preallocate_daily_slots(trading_date, total_slots)`(멱등) → `_last_fired_date = trading_date`. 반환 trading_date.
- `_maybe_tick()`: `now_tz = clock().astimezone(tz)`; `target = combine(now_tz.date(), schedule_time, tz)`; `now_tz < target` 면 skip; `_last_fired_date == now_tz.date()` 면 skip(오늘 이미 생성); 아니면 run_once.
- `run_forever(stop, tick_sec=60)`: boot run_once(당일 즉시 확보) 후 `stop.wait(tick_sec)` 루프 + try/except(루프 생존).
- 경계 의미: boot 는 항상 현재 TZ 일자 슬롯 확보(시각 무관). schedule_time 은 **다음날** 경계 fire 시점 게이트. 기본 00:00 → 자정 직후 다음날 생성.
## 2. 도메인 무침범 (A-3)
- `SlotService.preallocate_daily_slots(trading_date: date, count)` 시그니처/멱등 그대로 재사용. TZ 계산은 스케줄러(호출부)에서만 수행 → REQ-002 도메인 본문 변경 없음.
## 3. 설정 (REQ-048, TOML 일원화)
- `DaemonInstanceConfig`(TOML, frozen): `trading_timezone: str = "Asia/Seoul"`, `daily_preallocation_time: str = "00:00"`. `from_toml` 이 optional 키 파싱(부재 시 기본).
- `DaemonConfig`: 동일 두 필드(기본 KST/00:00). `from_env` 는 기본값 사용(TOML 일원화 — env var 미도입). `from_config` 가 TOML 값으로 `replace`.
- 운영자는 `--config` TOML 로 명시 지정(FR-4). 신규 설정 표면 아님.
## 4. daemon 배선 (REQ-005)
- `__init__`: `DailyPreallocationScheduler(slot_service, total_slots, tz=ZoneInfo(config.trading_timezone), schedule_time=_parse_kst_time(config.daily_preallocation_time))`.
- `setup()`: UTC 날짜 `preallocate_daily_slots` 직접 호출 제거 → `preallocation_scheduler.run_once()`(boot, TZ 당일 확보).
- `_preallocation_loop`: `run_forever(stop, tick_sec=60)`. workers 에 `trex-preallocation` 추가(8 스레드).
## 5. timestamp UTC 유지 (NFR-1)
- 슬롯 `created_at`/`updated_at`/전이 ts 는 기존대로 `datetime.now(tz=timezone.utc)`. 본 변경은 trading_date(date) 계산만 TZ 화.
## 6. baseline 문서 (REQ-009)
- runtime-flows: 데몬 lifecycle(TOML 키 추가·8 스레드·setup preallocation_scheduler) + `일일 슬롯 preallocation` Flow 신규.
- data-model: trading_date=설정 TZ 기준 / timestamp=UTC 구분.
- config/instance.example.toml: 두 키 주석 문서화.
## 멱등성/안정성 (NFR-2/3)
- 일자당 1회 fire + preallocate date 멱등 → 중복 tick·다중 인스턴스 중복 생성 0. 재시작 시 boot run_once 가 당일 확보. 무재시작 일자 경계는 _maybe_tick 가 다음날 생성.
## 테스트 설계
- `tests/application/test_preallocation_scheduler.py`(고정 clock 주입): KST/UTC 자정 경계 trading_date(S-1), timestamp UTC 유지(S-2), 일자 경계 자동 생성+멱등 재tick(S-3), 설정 TZ 변경(S-4), 트리거 시각 게이팅, idle 슬롯.
- `tests/runtime/test_instance_config.py` 확장: TOML 기본값(KST/00:00) + TOML 값 from_config 전파.
@@ -1,30 +0,0 @@
# BAT-018 Implementation
- Status: completed
- Batch: BAT-018
## 구현 요약
- dcy-014 trading_date 설정 타임존(KST) 분리 + 일일 슬롯 preallocation(issue #14)을 전부 change-req 로 구현(신규 REQ 없음; 스케줄러는 REQ-030 패턴 준용).
## 변경 내역
### REQ-005 — 스케줄러 + 부트스트랩
- `trex_core/application/preallocation_scheduler.py`(신규): `DailyPreallocationScheduler` — `run_once()`(TZ trading_date 멱등 preallocate), `_maybe_tick()`(TZ 일자당 1회 + 트리거 시각 게이트), `run_forever(stop, tick_sec)`. clock 주입식(테스트 결정론).
- `trex_core/runtime/daemon.py`: `DailyPreallocationScheduler` 인스턴스화(`tz=ZoneInfo(config.trading_timezone)`, `schedule_time=_parse_kst_time(config.daily_preallocation_time)`); `setup()` 의 UTC 날짜 직접 preallocation → `preallocation_scheduler.run_once()`; `_preallocation_loop` + `trex-preallocation` 워커(8 스레드); `zoneinfo.ZoneInfo` import. 미사용 `datetime, timezone` import 제거.
### REQ-048 — TOML 설정
- `trex_core/runtime/config.py`: `DaemonInstanceConfig` 에 `trading_timezone="Asia/Seoul"`/`daily_preallocation_time="00:00"` + `from_toml` optional 파싱; `DaemonConfig` 동일 필드(기본값); `from_config` 가 TOML 값 전파(`replace`). `from_env` 는 기본값.
- `config/instance.example.toml`: 두 키 주석 문서화.
### REQ-009 — baseline 문서
- `docs/runtime-flows.md`: 데몬 lifecycle TOML 키·8 스레드·setup preallocation_scheduler + `일일 슬롯 preallocation` Flow 신규. Last Updated → BAT-018.
- `docs/data-model.md`: trading_date=설정 TZ 기준 / timestamp=UTC 구분. Last Updated → BAT-018.
## 테스트
- 신규 `tests/application/test_preallocation_scheduler.py`(고정 clock): 6건 — KST/UTC 자정 경계 trading_date(S-1), timestamp UTC 유지(S-2), 일자 경계 자동 생성+멱등 재tick(S-3), 설정 TZ 변경(S-4), 트리거 시각 게이팅, idle 슬롯.
- `tests/runtime/test_instance_config.py` 확장: TOML 기본값(KST/00:00) + TOML 값 from_config 전파 2건.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `302 passed / 32 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `334 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -1,34 +0,0 @@
# BAT-018: trading_date 설정 타임존(KST) 분리 + 일일 슬롯 preallocation (issue #14)
- Status: released
- Profile: standard
- Source Discovery: dcy-014
- Included REQ: REQ-005 (CHG-20260608-01), REQ-048 (CHG-20260608-01), REQ-009 (CHG-20260608-01)
## Summary
- dcy-014(당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation, issue #14)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-014 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**(스케줄러는 REQ-030 SnapshotScheduler 패턴 준용).
- 핵심: `trading_date` 를 시스템 UTC 가 아니라 설정 타임존(기본 `Asia/Seoul`) 기준으로 계산하고, timestamp 는 UTC 유지. 데몬 시작 1회 UTC preallocation 을 `DailyPreallocationScheduler`(tick 루프 + TZ 일자당 1회 멱등 fire)로 대체해 재시작 없이 다음날 슬롯이 자동 생성된다.
## Scope
- In Scope:
- `trading_date` 의 설정 가능한 타임존 분리(기본 KST) + timestamp UTC 유지 (REQ-005)
- TZ 일자 경계 기준 일일 멱등 preallocation 스케줄러 + daemon 배선 (REQ-005)
- 타임존·트리거 시각 TOML 설정(`trading_timezone`/`daily_preallocation_time`) (REQ-048)
- runtime-flows/data-model 문서 갱신 (REQ-009)
- Out of Scope:
- timestamp 저장 형식 변경(UTC 유지)
- 슬롯 용량/total_slots 정책 변경
- 거래일 캘린더(주말·공휴일 비거래일 제외) — 매 캘린더일 생성으로 확정(OQ-2), 캘린더 도입은 후속 분리
- REQ-002(slot 도메인) 본문 변경 — `trading_date: date` 시그니처 유지, TZ 계산은 호출부(스케줄러)에서 수행(A-3)
## 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-014). OQ-1(기본 00:00 TZ 자정·TOML 설정)·OQ-2(매 캘린더일 생성)·OQ-3(전부 change-req) Answered.
- 스케줄러는 `SnapshotScheduler`(REQ-030)의 KST 일자당 1회 fire + tick 루프 패턴을 준용(A-1).
- 멱등성(A-2): `preallocate_daily_slots` 가 date 멱등 → 일일 재실행·다중 인스턴스·중복 tick 안전(NFR-2).
- 운영 설정은 기존 `--config` TOML(REQ-048)에 키 추가로 편입(신규 설정 표면 아님). from_env 는 KST/00:00 기본.
@@ -1,47 +0,0 @@
# BAT-018 Planning
- Status: confirmed
- Batch: BAT-018
- Profile: standard
## Included REQ
- REQ-005 (Installation, change-req): setup() UTC→TZ trading_date + `DailyPreallocationScheduler` 신규 + daemon 스레드 배선.
- REQ-048 (Configuration, change-req): TOML `trading_timezone`/`daily_preallocation_time` 키 + from_config 전파.
- REQ-009 (Documentation, change-req): runtime-flows/data-model 정합.
## Delivery Plan
1. REQ-005: `application/preallocation_scheduler.py`(SnapshotScheduler 패턴, TZ date + 일자당 1회 멱등 fire + boot run_once).
2. REQ-048: `config.py` DaemonInstanceConfig TOML 키 2종 + DaemonConfig 필드 + from_config 전파(+ from_env KST/00:00 기본).
3. REQ-005 배선: `daemon.py` setup() → `preallocation_scheduler.run_once()`, `_preallocation_loop` 스레드 + workers 등록, ZoneInfo(tz).
4. REQ-009: runtime-flows(데몬 lifecycle + 일일 preallocation Flow)/data-model(trading_date TZ vs UTC) + instance.example.toml.
5. 신규 테스트(test_preallocation_scheduler, test_instance_config 확장) + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-002/BAT-002 daemon 부트스트랩(REQ-005) — preallocation 지점.
- dcy-005/BAT-008 SnapshotScheduler(REQ-030) — 스케줄러 패턴 준용.
- dcy-009/BAT-013 DaemonInstanceConfig TOML(REQ-048) — 설정 키 추가.
- REQ-002 preallocate_daily_slots — date 멱등(재사용, 본문 변경 없음).
- External:
- stdlib `zoneinfo`(py3.12) — IANA 타임존.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) trading_date TZ 분리는 daemon 부트스트랩·스케줄러·설정·도메인 호출부 경계에 걸쳐, "TZ 는 호출부에서, 도메인 시그니처는 date 유지"(A-3) 경계를 설계에서 못박지 않으면 도메인 침범/회귀 위험. (b) 일자 경계 fire/멱등/트리거 시각 게이팅 규칙(SnapshotScheduler 준용)을 설계에서 고정해야 중복 생성·누락(NFR-2/3)을 막는다. (c) 설정 표면(TOML 일원화 + from_env 기본)·timestamp UTC 유지(NFR-1) 경계 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-005 DailyPreallocationScheduler, test_preallocation_scheduler green
2. M2 — REQ-048 TOML 키 + from_config 전파, test_instance_config 확장 green
3. M3 — REQ-005 daemon 배선(setup + 스레드)
4. M4 — REQ-009 문서 정합 + instance.example.toml
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (Low, OQ-1): 트리거 시점 운영 기대 불일치. 대응: 기본 00:00 + TOML 설정, SnapshotScheduler 패턴 재사용.
- R-2 (Medium): TZ 전환 경계에서 기존 UTC 날짜 슬롯과 새 TZ trading_date 불일치. 대응: 멱등 생성으로 중복 방지 + 경계 케이스 테스트(KST/UTC 경계).
- R-3 (Low, OQ-2 수용): 비거래일 idle 슬롯 누적. 대응: 매 캘린더일 생성 수용, idle 은 buy admit 시에만 활성화로 무해. 거래일 캘린더 후속.
## Notes
- 1차 게이트: 전체 회귀 green(부트스트랩 변경 무회귀).
- 경계 검증은 고정 clock 주입 단위로 결정론 확보(스레드 불필요).
- 멱등 preallocation 은 PG 게이트 전이/마이그레이션 테스트 회귀로도 간접 검증.
@@ -1,50 +0,0 @@
# BAT-018 Verification
- Status: verified
- Batch: BAT-018
## Acceptance Mapping
### REQ-005 (스케줄러 + 부트스트랩, change-req)
| AC | Evidence |
|----|----------|
| trading_date = 설정 TZ 기준(UTC 아님) | tests/application/test_preallocation_scheduler.py::test_trading_date_uses_kst_not_utc_at_midnight_boundary |
| 일자 경계 자동 생성(무재시작) + 멱등 | ::test_cross_day_boundary_auto_creates_next_day_idempotently |
| 트리거 시각 게이팅 | ::test_trigger_time_gates_next_day_creation |
| idle 슬롯 생성 | ::test_idle_slots_created_for_trading_date |
| 부트스트랩/스레드 배선 회귀 | 전체 회귀(daemon import/setup) |
### REQ-048 (TOML 설정, change-req)
| AC | Evidence |
|----|----------|
| TOML 기본값(KST/00:00) | tests/runtime/test_instance_config.py::test_trading_timezone_defaults_to_kst |
| TOML 값 from_config 전파 | ::test_trading_timezone_from_toml_propagates |
| 기존 TOML 키 회귀 | 기존 test_instance_config 전체 |
### REQ-009 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| runtime-flows/data-model 정합 | 문서 diff(BAT-018) + instance.example.toml |
## Evidence
- Test:
- 단위 전체 회귀: `302 passed / 32 skipped`(신규 스케줄러 6 + config 2 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `334 passed`.
- `ruff check trex_core tests` → All checks passed.
- Logs: `302 passed, 32 skipped, 1 deselected` / `334 passed, 1 deselected ~16.1s`.
## 성공 기준(dcy-014) 대응
- S-1(설정 TZ "오늘" = DB trading_date, UTC 자정 경계 포함): test_trading_date_uses_kst_not_utc_at_midnight_boundary(15:30Z → KST 6/9).
- S-2(timestamp UTC 유지): test_timestamps_stay_utc(updated_at utcoffset 0).
- S-3(무재시작 일자 경계 자동 생성 + 중복 0): test_cross_day_boundary_auto_creates_next_day_idempotently.
- S-4(TOML 타임존 설정 반영): test_configurable_timezone_changes_trading_date(UTC TZ → 6/8) + test_trading_timezone_from_toml_propagates.
## Carried-Over / 비차단
- 기본 타임존 `Asia/Seoul` 확정·TOML 키 명명(DATA): 구현에서 `trading_timezone`/`daily_preallocation_time` 로 확정. 운영 배포 안내는 instance.example.toml 에 문서화.
- 실 다중 인스턴스 동시 일일 preallocation 무중복(NFR-2): 멱등 preallocate 로 보장, 실 환경 동시 기동 e2e 는 운영 검증 비차단.
- 거래일 캘린더(주말/공휴일): OQ-2 결정으로 매 캘린더일 생성, 후속 분리.
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 부트스트랩 TZ 전환·일일 preallocation·TOML 설정 모두 단위 green(334 passed 회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
@@ -1,52 +0,0 @@
# BAT-019 Design
- Status: confirmed
- Batch: BAT-019
## 설계 목표
- portfolio-hub 404(미등록 포트폴리오)의 초당 무한 재시도(실측 930회)를 제거하고, 일시 오류는 절제된 백오프로 재시도하되 상한 초과 시 dead-letter 로 종료한다. 터미널 실패는 가시화한다(issue #13).
## 1. 오류 분류 (REQ-017)
- `LedgerPort` 에 `LedgerPermanentError(LedgerError)` 추가.
- `portfolio_hub.py`: 응답 분류 — 2xx 성공, 409→Conflict(불변), **404/400/422→Permanent**(`_PERMANENT_STATUS` frozenset), 그 외 4xx/5xx/network→Retryable. 408/429 는 permanent 집합에 없으므로 retryable 유지(OQ-1).
- `InMemoryLedger` 에 `permanent_executions` 주입 hook(테스트용).
## 2. outbox 상태머신 (REQ-019)
- OutboxOutcome 추가: `PERMANENT_FAILURE`, `DEAD_LETTERED`. 둘 다 WorkItemStatus 신규 `DELIVERY_FAILED`(단일 터미널, 사유는 last_outcome). `_DELIVERY_FAILED_OUTCOMES` frozenset.
- `LedgerWorkItem.next_retry_at: Optional[datetime]` 추가.
- `LedgerWriter._process`:
- `LedgerPermanentError` → `mark_outcome(PERMANENT_FAILURE)` + WARN → 터미널(재시도 0).
- `LedgerRetryableError` → `new_attempts = item.attempts+1`; `new_attempts >= max_attempts` 면 `DEAD_LETTERED`+WARN(터미널); 아니면 `next_retry_at = clock() + backoff(new_attempts)` 로 `RETRYABLE_FAILURE`(pending).
- conflict/완료/중복 경로 불변.
- `_backoff_delay(n) = min(base*2^(n-1), cap) * (1 + 0.25*rng())`. clock/rng 주입(결정론).
- `mark_outcome(..., next_retry_at=None)` 시그니처 확장 — retryable 일 때만 저장, 그 외 None.
## 3. list_pending 백오프 스킵 (REQ-019, NFR-3)
- InMemory: `status==PENDING and (next_retry_at is None or next_retry_at <= now)`. now 는 outbox 의 주입 clock(writer 와 공유해 결정론).
- PG: `WHERE status='pending' AND (next_retry_at IS NULL OR next_retry_at <= now())`. 부분 인덱스 `idx_ledger_outbox_pending_retry`.
## 4. 마이그레이션 007 (REQ-019)
- `ADD COLUMN IF NOT EXISTS next_retry_at TIMESTAMPTZ`.
- status CHECK: DROP/ADD constraint 로 `delivery_failed` 추가(idempotent). last_outcome CHECK: permanent_failure/dead_lettered 추가.
- 글롭 재적용 모델 → DROP CONSTRAINT IF EXISTS + ADD 로 재실행 안전.
## 5. 가시화 (REQ-018, NFR-2)
- `WriterStats.delivery_failed` 카운터. WARN 로그(permanent: reason / dead-letter: attempts+reason). status `delivery_failed` 로 pending 과 구분.
- dcy-011 `ledger_skipped` 패턴 정합; dead-letter 누락은 REQ-055 `find_ledger_gap_slots` 와 연계.
## 6. 설정 (REQ-019)
- `DaemonConfig`: `ledger_retry_base_sec`(1.0)/`ledger_retry_cap_sec`(300.0)/`ledger_max_attempts`(8). env `TREX_LEDGER_RETRY_BASE_SEC`/`_CAP_SEC`/`TREX_LEDGER_MAX_ATTEMPTS`. daemon 이 LedgerWriter 에 주입.
## 7. baseline 문서 (REQ-020)
- runtime-flows: ledger writer permanent/backoff/dead-letter + list_pending 스킵 + env.
- data-model: ledger_outbox status `delivery_failed`·`next_retry_at`·last_outcome 확장.
- interface-contract: LedgerPermanentError 오류 계약.
## 정합/멱등 (NFR-4)
- 단일 터미널 패턴(A-3): 409 conflict_blocked 와 동형 → outbox 처리 구조 최소 변경. correlation_id dedup(REQ-019) 불변.
## 테스트 설계
- adapter: 404/400/422→permanent(param), 429→retryable, 409→conflict, 5xx/network→retryable.
- outbox(programmable LedgerPort + 주입 clock/rng): permanent 즉시 터미널·재시도 0, retryable 백오프 스킵→due 후 재시도, max_attempts dead-letter, WARN 가시화, 백오프 행이 신규 행 비잠식.
- PG 게이트: delivery_failed permanent 영속, 백오프 행 list_pending 스킵, 기존 retryable(base=0 즉시 due) 정합.
- 회귀: 기존 retryable 테스트(test_ledger_outbox/test_execution_ledger)에 공유 clock 주입으로 백오프 정합.
@@ -1,42 +0,0 @@
# BAT-019 Implementation
- Status: completed
- Batch: BAT-019
## 구현 요약
- dcy-015 ledger outbox 영속 오류 분류 + 백오프/dead-letter + 가시화(issue #13)를 전부 change-req 로 구현(신규 REQ 없음; 백오프 env 는 REQ-019 흡수).
## 변경 내역
### REQ-017 — 오류 분류
- `trex_core/application/ledger_port.py`: `LedgerPermanentError(LedgerError)` 추가.
- `trex_core/adapters/ledger/portfolio_hub.py`: `_PERMANENT_STATUS={400,404,422}` → `LedgerPermanentError`; 409 conflict·408/429/5xx/network retryable 불변. 헤더 계약 갱신.
- `trex_core/adapters/ledger/in_memory.py`: `permanent_executions` 주입 hook.
### REQ-019 — outbox 백오프/dead-letter
- `trex_core/application/ledger_outbox.py`:
- OutboxOutcome `PERMANENT_FAILURE`/`DEAD_LETTERED`; WorkItemStatus `DELIVERY_FAILED`(단일 터미널); `_DELIVERY_FAILED_OUTCOMES`.
- `LedgerWorkItem.next_retry_at`.
- `LedgerWriter(retry_base_sec/retry_cap_sec/max_attempts/clock/rng)` + `_backoff_delay` + `_process`(permanent→터미널, retryable→백오프/dead-letter).
- `mark_outcome(..., next_retry_at=...)`; `WriterStats.delivery_failed`.
- InMemory outbox: `clock` 주입, `list_pending` 백오프 스킵, mark_outcome 신규 outcome/next_retry_at.
- `trex_core/adapters/persistence/postgresql.py`: `_OUTBOX_COLUMNS` next_retry_at; `list_pending` `next_retry_at IS NULL OR <= now()` 스킵; `mark_outcome` permanent/dead-letter→delivery_failed + next_retry_at; `_row_to_item` next_retry_at.
- `trex_core/adapters/persistence/sql/007_ledger_outbox_delivery_failed_and_backoff.sql`(신규): next_retry_at 컬럼 + status/last_outcome CHECK 확장(idempotent) + 부분 인덱스.
- `trex_core/runtime/config.py`: `ledger_retry_base_sec`/`_cap_sec`/`ledger_max_attempts` + env. `trex_core/runtime/daemon.py`: LedgerWriter 에 주입.
### REQ-018 — 가시화
- `ledger_outbox.py` writer: permanent/dead-letter WARN 로그 + `WriterStats.delivery_failed` 카운터, status `delivery_failed` 구분.
### REQ-020 — baseline 문서
- `docs/runtime-flows.md`(ledger writer permanent/backoff/dead-letter + list_pending 스킵 + env), `docs/data-model.md`(ledger_outbox status/next_retry_at/last_outcome), `docs/interface-contract.md`(LedgerPermanentError). Last Updated → BAT-019.
## 테스트
- 신규 `tests/adapters/ledger/test_portfolio_hub_ledger.py` 확장: 404/400/422 permanent(param) + 429 retryable.
- 신규 `tests/application/test_ledger_outbox_backoff.py`: permanent 즉시 터미널·재시도 0, retryable 백오프 스킵→due 재시도, max_attempts dead-letter, WARN 가시화, 백오프 행 비잠식(5건).
- 신규 PG 게이트 `tests/adapters/persistence/test_postgresql_ledger_outbox.py` 확장: delivery_failed permanent 영속, 백오프 list_pending 스킵, 기존 retry(base=0) 정합.
- 회귀 정합: `test_ledger_outbox.py`/`test_execution_ledger.py` 의 retryable 테스트에 공유 clock 주입(백오프 advance).
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `311 passed / 34 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `345 passed`(마이그레이션 007 + delivery_failed/backoff PG round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
@@ -1,34 +0,0 @@
# BAT-019: ledger outbox 영속 오류 분류 + 백오프/dead-letter + 가시화 (issue #13)
- Status: released
- Profile: standard
- Source Discovery: dcy-015
- Included REQ: REQ-017 (CHG-20260608-01), REQ-019 (CHG-20260608-01), REQ-018 (CHG-20260608-03), REQ-020 (CHG-20260608-01)
## Summary
- dcy-015(portfolio-hub 404 가 retryable 로 분류되어 백오프·상한 없이 영구 재시도, issue #13)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-015 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**(백오프 env 는 REQ-019 에 흡수).
- 핵심: portfolio-hub 어댑터가 **404/400/422 → `LedgerPermanentError`** 로 분류하고, outbox 가 이를 터미널 `delivery_failed` 로 즉시 처리(재시도 0). 일시 오류는 **지수 백오프+jitter+max_attempts** 로 재시도하되 상한 초과 시 `delivery_failed`(dead-letter). `list_pending` 이 `next_retry_at` 미도래 행을 건너뛰어 FIFO head-of-line 잠식을 막고, 터미널 실패는 WARN+카운터로 가시화한다.
## Scope
- In Scope:
- 영속 오류 분류 분리(404/400/422 → `LedgerPermanentError`) (REQ-017)
- retryable 지수 백오프+jitter + `next_retry_at` + max_attempts + 단일 터미널 `delivery_failed`(permanent/dead-letter, 사유 구분) + `list_pending` 백오프 스킵 + status enum/컬럼 마이그레이션(007) + 백오프 env (REQ-019)
- 터미널/dead-letter WARN+메트릭 가시화 (REQ-018)
- runtime-flows/data-model/interface-contract 정합 (REQ-020)
- Out of Scope:
- safety-sell→KIS 영속 거부 backoff(dcy-010/REQ-052, 경로 다름)
- portfolio_id 누락 ledger skip(dcy-011/#11)
- dead-letter 행 재처리 트리거(운영 명령/자동 재처리) — 재처리 가능한 상태 제공까지, 트리거는 후속 분리
## 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-015). OQ-1(404/400/422 영속)·OQ-2(지수 백오프+next_retry_at·max-attempts env·단일 `delivery_failed`)·OQ-3(전부 change-req) Answered.
- 단일 터미널 패턴(A-3): 409 `conflict_blocked` 와 동일하게 `delivery_failed` 단일 status + last_outcome(permanent_failure/dead_lettered)로 사유 구분 → 스키마 최소 변경.
- 가시화(A-1): dcy-011 `ledger_skipped` 카운터·`find_ledger_gap_slots`(REQ-055) 패턴 정합.
- dead-letter 는 폐기 아님 — 운영자 재처리 가능 상태(R-3 완화).
- 마이그레이션 007 은 글롭 재적용 모델 → ADD COLUMN IF NOT EXISTS + DROP/ADD CONSTRAINT 로 idempotent.
@@ -1,47 +0,0 @@
# BAT-019 Planning
- Status: confirmed
- Batch: BAT-019
- Profile: standard
## Included REQ
- REQ-017 (Integration, change-req): 404/400/422 → `LedgerPermanentError` 분류 + LedgerPort 타입.
- REQ-019 (Exception, change-req): 백오프+jitter+`next_retry_at`+max_attempts+`delivery_failed`+마이그레이션 007+`list_pending` 스킵+env.
- REQ-018 (Integration, change-req): 터미널/dead-letter WARN+카운터 가시화.
- REQ-020 (Documentation, change-req): runtime-flows/data-model/interface-contract 정합.
## Delivery Plan
1. REQ-017: `ledger_port.py` `LedgerPermanentError`; `portfolio_hub.py` 404/400/422 매핑 + 헤더; `in_memory.py` permanent 주입 hook.
2. REQ-019: `ledger_outbox.py` OutboxOutcome(permanent_failure/dead_lettered)+WorkItemStatus(delivery_failed)+`next_retry_at`+LedgerWriter 백오프/dead-letter+`list_pending` 스킵+InMemory clock; `postgresql.py` next_retry_at/status/list_pending skip/mark_outcome; `sql/007_*.sql`; `config.py` env + daemon 배선.
3. REQ-018: writer WARN + `WriterStats.delivery_failed`.
4. REQ-020: 문서 3종 정합.
5. 테스트(adapter classify, outbox backoff/dead-letter/permanent/visibility, PG delivery_failed/skip, 기존 retryable 백오프 정합 migration) + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-004/BAT-006 LedgerPort/outbox/writer(REQ-017/018/019) — 변경 대상.
- dcy-011/BAT-015 ledger skip 가시화·REQ-055 — 가시화 패턴 정합(A-1).
- dcy-010/BAT-014 백오프 env 관례(A-2, 경로 다름).
- External:
- PostgreSQL(status enum/next_retry_at 마이그레이션). portfolio-hub 4xx/5xx 규약(R-1, 운영 확인).
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) 오류 분류 분리 + outbox 상태머신 확장(영속/백오프/dead-letter)이 LedgerPort·어댑터·outbox·PG·마이그레이션에 교차 — 단일 터미널 `delivery_failed`(사유 구분) 경계와 `next_retry_at` 스킵 규칙을 설계에서 못박지 않으면 상태 정합(R-2)·과대 분류(R-1) 위험. (b) 백오프 공식·max_attempts 전이·clock 주입(결정론 테스트) 경계 확정. (c) status enum CHECK 마이그레이션 idempotency(글롭 재적용) 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-017 분류 + LedgerPermanentError, adapter 테스트 green
2. M2 — REQ-019 outbox 백오프/dead-letter/next_retry_at + InMemory, backoff 테스트 green
3. M3 — REQ-019 PG next_retry_at/status/skip + 마이그레이션 007, PG 게이트 green
4. M4 — REQ-018 가시화 + REQ-020 문서
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (Medium): 영속 분류 과대 → 정상 거래 ledger 영구 실패. 대응: 404/400/422 한정(OQ-1) + dcy-011 admission portfolio_id 검증으로 등록-지연 404 감소 + dead-letter 가 운영자 재처리 가능 상태.
- R-2 (Medium): status enum 마이그레이션이 기존 conflict_blocked/pending/completed 와 부정합. 대응: 기존 터미널 패턴 그대로 따르는 단일 새 터미널 + 마이그레이션/전이 테스트.
- R-3 (Medium): dead-letter 단순 포기 시 ledger 영구 누락 고착. 대응: 운영자 재처리 가능 상태 + REQ-055 find_ledger_gap_slots·skip 가시화 연계.
## Notes
- 1차 게이트: 전체 회귀 green(백오프 도입이 기존 retryable 테스트에 미치는 영향 정합).
- 결정론: LedgerWriter/InMemory outbox 에 clock 주입 + rng 주입(jitter)로 백오프 테스트 결정화.
- PG list_pending 백오프 스킵은 인프라 게이트로 검증.
@@ -1,57 +0,0 @@
# BAT-019 Verification
- Status: verified
- Batch: BAT-019
## Acceptance Mapping
### REQ-017 (오류 분류, change-req)
| AC | Evidence |
|----|----------|
| 404/400/422 → LedgerPermanentError | tests/adapters/ledger/test_portfolio_hub_ledger.py::test_client_errors_are_permanent |
| 429 → retryable (영속 아님) | ::test_429_stays_retryable |
| 409 → conflict / 5xx·network → retryable 회귀 | ::test_409_surfaces_as_ledger_conflict_error / ::test_5xx_is_retryable / ::test_network_error_is_retryable |
### REQ-019 (백오프/dead-letter, change-req)
| AC | Evidence |
|----|----------|
| 영속 → delivery_failed 즉시(재시도 0) | tests/application/test_ledger_outbox_backoff.py::test_permanent_error_terminates_immediately_no_retry |
| retryable 백오프 + next_retry_at 스킵 | ::test_retryable_backoff_skips_until_next_retry_at |
| max_attempts → dead-letter delivery_failed | ::test_retryable_dead_letters_after_max_attempts |
| 백오프 행 FIFO 비잠식 | ::test_backoff_row_does_not_block_new_rows |
| PG delivery_failed 영속 + 마이그레이션 007 | tests/adapters/persistence/test_postgresql_ledger_outbox.py::test_writer_marks_delivery_failed_permanent_on_pg |
| PG 백오프 list_pending 스킵 | ::test_backoff_row_skipped_by_pending_scan_on_pg |
| 기존 retryable 정합(백오프) | test_ledger_outbox.py::test_writer_retries_until_success / test_execution_ledger.py::test_retryable_failure_then_eventually_completes / PG::test_writer_retries_on_pg |
### REQ-018 (가시화, change-req)
| AC | Evidence |
|----|----------|
| 터미널 delivery_failed WARN + 카운터 | test_ledger_outbox_backoff.py::test_permanent_failure_emits_warning (+ WriterStats.delivery_failed 단언 전반) |
### REQ-020 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| runtime-flows/data-model/interface-contract 정합 | 문서 diff(BAT-019) |
## Evidence
- Test:
- 단위 전체 회귀: `311 passed / 34 skipped`(신규 backoff 5 + adapter 4 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `345 passed`(마이그레이션 007 적용 + delivery_failed/backoff PG round-trip 포함).
- `ruff check trex_core tests` → All checks passed.
- Logs: `311 passed, 34 skipped, 1 deselected` / `345 passed, 1 deselected ~17.4s`.
## 성공 기준(dcy-015) 대응
- S-1(404 즉시 터미널, attempts 폭증 없음): test_permanent_error_terminates_immediately_no_retry(ledger.calls==1) + PG::test_writer_marks_delivery_failed_permanent_on_pg.
- S-2(일시 오류 백오프 + max_attempts dead-letter, 무한 1초 재시도 0): test_retryable_backoff_skips_until_next_retry_at + test_retryable_dead_letters_after_max_attempts.
- S-3(터미널/dead-letter WARN·구분 status 가시화): test_permanent_failure_emits_warning + delivery_failed status 단언.
- S-4(백오프/영속 행 FIFO 비잠식): test_backoff_row_does_not_block_new_rows + PG::test_backoff_row_skipped_by_pending_scan_on_pg.
## Carried-Over / 비차단
- R-1(영속 분류 과대): 404/400/422 한정 + dcy-011 admission portfolio_id 검증으로 등록-지연 404 감소 + dead-letter 운영자 재처리 가능. portfolio-hub 실 4xx/5xx 규약(DATA)은 운영 검증 비차단.
- dead-letter 재처리 트리거(운영 명령/자동화)는 Out of Scope(후속 분리) — 본 주기는 재처리 가능한 `delivery_failed` 상태 제공까지.
- 실 portfolio-hub full-stack(미등록 portfolio 404) e2e 는 운영 검증 비차단(InMemory permanent 주입 + adapter status 매핑으로 단위 검증).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. 영속 분류·백오프·dead-letter·가시화 모두 단위 green + PG 마이그레이션 round-trip(345 passed). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
@@ -1,45 +0,0 @@
# BAT-020 Design
- Status: confirmed
- Batch: BAT-020
## 설계 목표
- 운영자가 CLI 로 조회·매수·매도·일시정지를 수행하되, 매매는 데몬 admission(단일 writer)을 거쳐 불변식을 보존한다. 운영자 조회가 전략 생존 신호로 오인되지 않게 한다(R-4).
## 1. 워치독 억제 (REQ-021/034, 서버)
- `make_snapshot_rpc_handler`: `touch_watchdog = bool(body.get("touch_watchdog", True))`; `actor=="operator"` 면 강제 false. `watchdog_store and touch_watchdog` 일 때만 holding 슬롯 `last_seen_at` 갱신.
- 기본값(플래그 미지정) = 현행(전략 폴링=갱신) → 전략 모듈 무변경(하위호환). 운영자 CLI 는 항상 억제.
## 2. CLI 구조 (REQ-032/022)
- `main(argv, *, rpc=None, confirm=None)`: `rpc(queue, payload, *, correlation_id, timeout)->dict`, `confirm(prompt)->bool` 주입 가능(테스트 broker-free). 미주입 시 config 기반 transport(`call_rpc`) + 대화형 confirm.
- 인스턴스 라우팅 `_resolve_instance`: `--instance` > `--config`(TOML instance_key) > `default`(OQ-1).
- 큐: `topology.snapshot_queue(instance)`/`intent_queue(instance)`.
- pause/resume/paused: 기존 `PostgreSQLPauseStore` direct-DB 유지(OQ-3).
## 3. 명령별 매핑
- `universe`: snapshot RPC `{view:"all", actor:"operator", touch_watchdog:false, trading_date}` → assets 출력.
- `slots [--status held|buyable|all]`: snapshot RPC(억제) → assets; `holding`→`held` view 매핑.
- `enter <portfolio_id> [--capital/--qty/--entry-price/--strategy-id/--stop-loss/--take-profit]`: buy intent RPC(portfolio_id 필수, symbol 서버 resolve; symbol 미전송).
- `exit <slot_id> [--price/--reason]`: sell intent RPC(slot_id + optional limit_price; 미지정 시 시장가).
- trading_date 는 설정 타임존(dcy-014) 기준 today.
## 4. enter/exit 안전장치 (REQ-032, OQ-2) — `_intent_guard`
- `--dry-run`: resolved intent + 대상 인스턴스만 출력, 미제출(exit 0).
- live(`broker=kis`+allow_live) + `--live` 없음 → 에러(exit 2), 미제출.
- `--yes` 없음: `--json`(비대화형)이면 에러(exit 2); 아니면 confirm 프롬프트(기본 No) — 거부 시 미제출(exit 1).
- 통과 시 intent RPC 제출. admission 응답(`status/slot_id/reason`) 표시, `admitted` 아니면 exit 1.
- `_broker_mode(cfg)` → (broker, is_live). is_live = kis + allow_live.
## 5. 출력 (REQ-032)
- 사람용: 요약/표. `--json`: RPC 응답을 그대로 JSON(`json.dumps`). 실패: stderr 에 error(json 모드는 `{"error":...}`), exit 3.
## 6. baseline 문서 (REQ-026)
- interface-contract: 운영자 CLI↔RPC 매핑 표 + snapshot 요청 `touch_watchdog`/`actor` 플래그.
- runtime-flows: 운영자 CLI flow(라우팅/억제/안전장치/실패).
## 범위 밖 정합
- snapshot envelope 은 asset-centric(REQ-028 strict no-price) — slot_id 미포함. `exit` 는 운영자 제공 slot_id 를 intent RPC 로 전달(snapshot 에 slot_id 노출은 별도 계약 변경, 본 주기 밖). `slots` 는 보유 자산 상황 인지용.
## 테스트 설계
- `tests/runtime/test_cli_operator.py`(주입 rpc recorder/confirm): universe 억제 플래그/큐, slots held view, 인스턴스 라우팅, `--json` 유효성, enter 확인 거부 미제출/`--yes` 제출/rejected 비0/`--dry-run`/`--json` 요구 `--yes`, exit 가격/시장가, live 이중 ack(gate), RPC 실패 표시.
- `tests/application/test_messaging_snapshot_consumer.py`: 기본 요청 watchdog touch / operator 요청 suppress.
@@ -1,36 +0,0 @@
# BAT-020 Implementation
- Status: completed
- Batch: BAT-020
## 구현 요약
- dcy-016 운영자 CLI 확장(universe/slots/enter/exit, 데몬 RPC 통일) + 운영자 조회 워치독 억제를 전부 change-req 로 구현(신규 REQ 없음).
## 변경 내역
### REQ-021/034 — 워치독 억제 (서버)
- `trex_core/application/messaging_handlers.py::make_snapshot_rpc_handler`: `touch_watchdog = bool(body.get("touch_watchdog", True))`; `actor=="operator"` → 강제 false. `watchdog_store and touch_watchdog` 일 때만 holding `last_seen_at` 갱신. 기본값=현행(전략 폴링 갱신, 하위호환).
### REQ-032/022 — 운영자 CLI
- `trex_core/runtime/cli.py`(재작성): `main(argv, *, rpc=None, confirm=None)` 주입 가능.
- 라우팅 `_resolve_instance`(`--instance` > `--config` instance_key > `default`).
- `universe`/`slots`: snapshot RPC(`actor="operator"`/`touch_watchdog=false`), `holding`→`held`.
- `enter <portfolio_id>`: buy intent RPC(portfolio_id 필수, symbol 미전송 — 서버 resolve).
- `exit <slot_id>`: sell intent RPC(optional `--price`→limit_price, 미지정 시 시장가).
- 안전장치 `_intent_guard`: `--dry-run`(미제출), live(`broker=kis`+allow_live)는 `--yes --live` 이중 ack, `--yes` 없으면 confirm(기본 No)/`--json` 비대화형은 `--yes` 필수.
- `--json` 출력, RPC 실패는 stderr error + exit 3. admission rejected → exit 1.
- pause/resume/paused: 기존 `PostgreSQLPauseStore` direct-DB 유지(OQ-3).
- transport RPC: config 기반 `RabbitMQTransport`/`InMemoryTransport` `call_rpc` 재사용(REQ-022).
### REQ-026 — baseline 문서
- `docs/interface-contract.md`: 운영자 CLI↔RPC 매핑 표 + snapshot 요청 `touch_watchdog`/`actor` 플래그. Last Updated → BAT-020.
- `docs/runtime-flows.md`: 운영자 CLI flow(라우팅/억제/안전장치/실패). Last Updated → BAT-020.
## 테스트
- 신규 `tests/runtime/test_cli_operator.py`(주입 rpc/confirm, broker/DB free): 14건 — universe 억제 플래그/큐, slots held view, 인스턴스 라우팅, `--json` 유효성, enter 확인거부 미제출/`--yes` 제출/rejected 비0/`--dry-run`/`--json` 요구 `--yes`, exit 가격/시장가, live 이중 ack(gate+proceed), RPC 실패 표시.
- `tests/application/test_messaging_snapshot_consumer.py` 확장: 기본 요청 watchdog touch / operator 요청 suppress 2건.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `327 passed / 34 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `361 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -1,34 +0,0 @@
# BAT-020: 운영자 CLI 확장 (universe/slots/enter/exit) + 워치독 억제 (dcy-016)
- Status: released
- Profile: standard
- Source Discovery: dcy-016
- Included REQ: REQ-032 (CHG-20260608-01), REQ-022 (CHG-20260608-01), REQ-021 (CHG-20260608-02), REQ-034 (CHG-20260608-01), REQ-026 (CHG-20260608-01)
## Summary
- dcy-016(운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume, 데몬 RPC 통일)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-016 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**.
- 핵심: `trex-core` CLI 에 `universe`/`slots`(snapshot RPC)·`enter`(buy intent RPC)·`exit`(sell intent RPC) 추가. 전부 데몬 RabbitMQ RPC 로 통일해 단일 writer admission·불변식 보존(DB 직접 쓰기 없음). 운영자 조회는 `actor="operator"`/`touch_watchdog=false` 로 워치독 생존 신호를 억제(R-4). enter/exit 안전장치(확인 프롬프트/`--yes`/`--yes --live`/`--dry-run`). pause/resume 은 기존 direct-DB 유지.
## Scope
- In Scope:
- `universe`/`slots`/`enter`/`exit` CLI(전부 데몬 RPC) + 응답 표시 (REQ-032)
- CLI request-reply RPC 클라이언트 재사용 (REQ-022)
- snapshot 요청 워치독 억제 플래그(`touch_watchdog`/`actor`) (REQ-021) + 핸들러 억제 동작 (REQ-034)
- enter/exit 안전장치(확인/`--yes`/live 이중 ack/`--dry-run`) + 인스턴스 라우팅(`--config`/`--instance`/default) + 전 명령 `--json`
- interface-contract/runtime-flows 정합 (REQ-026)
- Out of Scope:
- 신규 서버측 RPC 표면/도메인 로직(기존 표면 재사용 — 워치독 억제만 snapshot 핸들러 국소 변경)
- buy/sell 계약 자체 변경(dcy-011/dcy-012)
- kill_switch CLI 노출(별도 결정)
- pause/resume 의 control RPC 통일(미채택 — 기존 direct-DB 유지, OQ-3)
## 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-016). OQ-1(`--config`+`--instance`/default)·OQ-2(안전장치 A안)·OQ-3(전부 change-req)·OQ-4(워치독 플래그) Answered.
- snapshot envelope 은 asset-centric(REQ-028 strict no-price) — slot_id 미포함. 따라서 `exit <slot_id>` 는 운영자 제공 slot_id 를 intent RPC 로 전달(snapshot 에 slot_id 노출은 별도 계약 변경이라 본 주기 밖). `slots` 는 보유 자산(asset_id/symbol/qty) 상황 인지용.
- 테스트 결정론: CLI 는 `rpc`/`confirm` 주입 가능(no broker/DB), snapshot 핸들러는 fake watchdog recorder.
@@ -1,49 +0,0 @@
# BAT-020 Planning
- Status: confirmed
- Batch: BAT-020
- Profile: standard
## Included REQ
- REQ-032 (Interface, change-req): CLI universe/slots/enter/exit + 인스턴스 라우팅 + enter/exit 안전장치 + `--json`.
- REQ-022 (Integration, change-req): CLI request-reply RPC 클라이언트(`call_rpc`) 재사용.
- REQ-021 (Integration, change-req): snapshot 요청 워치독 억제 플래그.
- REQ-034 (Interface, change-req): 핸들러가 억제 플래그 시 `last_seen_at` 미갱신.
- REQ-026 (Documentation, change-req): interface-contract/runtime-flows 정합.
## Delivery Plan
1. REQ-021/034(서버): `messaging_handlers.py::make_snapshot_rpc_handler` 에 `touch_watchdog`/`actor` 억제 분기(기본 현행 유지).
2. REQ-032/022(CLI): `cli.py` 재작성 — pause/resume/paused(direct-DB 유지) + universe/slots/enter/exit(RPC). 인스턴스 라우팅(`_resolve_instance`), RPC 빌드/주입(`rpc`), 안전장치(`_intent_guard`: confirm/`--yes`/`--yes --live`/`--dry-run`), `--json`, 실패 표시.
3. REQ-026(문서): interface-contract(CLI↔RPC 매핑 + 억제 플래그), runtime-flows(운영자 CLI flow).
4. 테스트: CLI(주입 rpc/confirm) + snapshot 핸들러 억제 + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-008/BAT-008 CLI(REQ-032 pause/resume) — 확장 대상.
- dcy-006/BAT-007 transport `call_rpc`(REQ-022) — 재사용.
- dcy-005/BAT-008 snapshot 핸들러/envelope(REQ-028/021) + 워치독(REQ-034) — 억제 대상.
- dcy-011 buy(portfolio_id 필수)·dcy-012 sell(slot_id+optional price) 계약 — CLI payload 근거.
- dcy-009 `_instance_queue`(REQ-049) — 라우팅.
- External:
- RabbitMQ(실 RPC). 단위는 주입 rpc 로 broker-free.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) enter/exit 는 실주문 표면(R-1) — 안전장치(확인/`--yes`/live 이중 ack/`--dry-run`)와 `--json` 비대화형 상호작용 규칙을 설계에서 못박지 않으면 오발주 위험. (b) 워치독 억제는 snapshot 핸들러 서버 동작 변경(REQ-034) — 기본값 현행 유지(하위호환) 경계와 플래그 의미를 설계에서 고정해야 안전 기능(safety-sell) 무력화/회귀를 막는다. (c) 인스턴스 라우팅(`--config`/`--instance`/default) 우선순위와 테스트용 rpc/confirm 주입 구조를 설계에서 명시. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-021/034 snapshot 억제 분기, 핸들러 테스트 green
2. M2 — REQ-032/022 CLI universe/slots/enter/exit + 안전장치, CLI 테스트 green
3. M3 — REQ-026 문서 정합
4. M4 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): enter/exit 오발주/실주문. 대응: 확인 프롬프트 기본 ON·`--yes`(paper)·`--yes --live` 이중 ack·`--dry-run`·rejected 표시·대상 인스턴스 항상 표시. admission portfolio_id 검증(dcy-011)이 1차 방어선.
- R-2 (Medium): 잘못된 인스턴스 라우팅. 대응: `--config` canonical + `--instance` override + default + 출력에 대상 인스턴스 표시.
- R-3 (Low): RPC 통일로 데몬 미가동 시 조회 불가. 대응: 의도된 트레이드오프 문서화 + 명확한 에러 표시.
- R-4 (High): 운영자 조회의 워치독 부수효과로 safety-sell 무력화. 대응: 억제 플래그(REQ-021/034), 기본값 현행 유지.
## Notes
- 1차 게이트: 전체 회귀 green(워치독 기본 동작 무회귀).
- CLI 단위는 broker/DB 없이 rpc/confirm 주입으로 결정론 확보.
- 실 RabbitMQ 왕복 e2e 는 기존 transport 인프라 게이트로 커버(call_rpc 자체).
@@ -1,60 +0,0 @@
# BAT-020 Verification
- Status: verified
- Batch: BAT-020
## Acceptance Mapping
### REQ-032 (운영자 CLI, change-req)
| AC | Evidence |
|----|----------|
| universe snapshot RPC 조회(직접 DB 없음) | tests/runtime/test_cli_operator.py::test_universe_uses_snapshot_queue_with_operator_suppression |
| slots view 매핑(holding→held) | ::test_slots_status_holding_maps_to_held_view |
| enter buy intent RPC 제출 | ::test_enter_with_yes_submits_buy_intent |
| exit sell intent RPC 제출(가격/시장가) | ::test_exit_with_yes_submits_sell_with_limit_price / ::test_exit_market_when_no_price |
| enter/exit 안전장치(확인/dry-run/json 요구 yes) | ::test_enter_aborts_without_confirmation_no_rpc / ::test_enter_dry_run_does_not_submit / ::test_enter_json_requires_yes |
| live 이중 ack | ::test_live_requires_live_flag / ::test_live_proceeds_with_yes_live |
| 인스턴스 라우팅 | ::test_instance_shortcut_routes_to_instance_queue |
| rejected 비0 종료 | ::test_enter_rejected_returns_nonzero |
### REQ-022 (CLI RPC 클라이언트, change-req)
| AC | Evidence |
|----|----------|
| call_rpc 재사용(주입 가능) | test_cli_operator.py 전반(주입 rpc recorder) |
| RPC 실패 명확 표시 | ::test_rpc_failure_surfaces_error |
### REQ-021/034 (워치독 억제, change-req)
| AC | Evidence |
|----|----------|
| 기본 요청 watchdog touch | tests/application/test_messaging_snapshot_consumer.py::test_strategy_poll_touches_watchdog_by_default |
| operator 요청 suppress | ::test_operator_query_suppresses_watchdog_touch |
| CLI 가 억제 플래그 전송 | test_cli_operator.py::test_universe_uses_snapshot_queue_with_operator_suppression |
### REQ-026 (baseline 문서, change-req)
| AC | Evidence |
|----|----------|
| interface-contract/runtime-flows 정합 | 문서 diff(BAT-020) |
## Evidence
- Test:
- 단위 전체 회귀: `327 passed / 34 skipped`(신규 CLI 14 + 워치독 2 포함; `::test_sandbox_token_and_submit` deselect).
- 인프라 게이트(PG trex_core_test + RabbitMQ): `361 passed`.
- `ruff check trex_core tests` → All checks passed.
- Logs: `327 passed, 34 skipped, 1 deselected` / `361 passed, 1 deselected ~17.8s`.
## 성공 기준(dcy-016) 대응
- S-1(universe/slots snapshot RPC 조회, 직접 DB 없음): test_universe_*/test_slots_*.
- S-2(enter/exit intent RPC 제출 + admitted/rejected 표시, DB 쓰기 0): test_enter_with_yes_*/test_exit_*/test_enter_rejected_*.
- S-3(확인 프롬프트/`--yes`/live 가드, 미확인 시 미제출): test_enter_aborts_*/test_live_requires_live_flag/test_live_proceeds_*.
- S-4(인스턴스 라우팅 + 실패 표시): test_instance_shortcut_*/test_rpc_failure_surfaces_error.
- S-5(`--json` 유효 + enter/exit `--json` 요구 `--yes`): test_json_output_is_valid_json/test_enter_json_requires_yes.
## Carried-Over / 비차단
- 실 RabbitMQ 왕복 CLI e2e: 기존 transport `call_rpc` 인프라 게이트로 커버(CLI 는 주입 rpc 단위). 실 다중 인스턴스 라우팅 e2e 는 운영 검증 비차단.
- snapshot envelope 에 slot_id 미노출 → `exit` 는 운영자 제공 slot_id 사용. snapshot 에 slot_id 노출은 REQ-028 strict shape 변경이라 본 주기 밖(별도 결정).
- kill_switch CLI 노출은 Out of Scope(후속).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음. CLI 명령·안전장치·워치독 억제 모두 단위 green(361 passed 회귀, 워치독 기본 동작 무회귀). LIVE sandbox 토큰 deselect 는 `.env` 환경 의존(본 batch 무관).
@@ -1,41 +0,0 @@
# BAT-021 Design
- Status: confirmed
- Batch: BAT-021
## 설계 목표
- 전략 이탈로 슬롯이 워치독(안전망) 소유로 넘어간 상태와 safety-sell 발동을 무성에서 가시화한다(issue #12). 가시화는 best-effort — 판정·청산을 절대 막지 않는다(NFR-1).
## 1. 워치독 상태 노출 (REQ-034/021)
- `WatchdogStore.last_seen(slot_id) -> Optional[datetime]` read-only getter 추가(판정 로직 불변).
- `make_snapshot_rpc_handler(..., watchdog_timeout_sec=120)` + `_attach_watchdog(payload, slot_repo, day, watchdog_store, timeout)`:
- holding 슬롯마다 `{slot_id, portfolio_id, symbol, expired(is_expired), last_seen}` 산출 → `payload["watchdog"]`.
- try/except 로 감싸 실패 시 응답 불변(best-effort). envelope·stub 두 경로 모두 attach.
- additive top-level 필드라 REQ-028 strict asset shape(no-price) 불변.
## 2. safety-sell 가시화 (REQ-037/038/039, OQ-2)
- `SafetySellService(instance_key="default")` + `self._owned: set` + `self.counters: dict`.
- `_log_fire(slot, reason)`: `_WARN_REASONS={forced_close_at, stop_loss}` → WARN, 그 외(take_profit, market_close_liquidation) → INFO. 구조화 필드(phase/slot_id/portfolio_id/reason/instance_key) + `counters["fire:{reason}"]++`. check_forced_close/check_prices/check_market_close 의 발동 지점에서 호출.
- `_update_ownership(now)`: grace 중이면 skip. `current = {holding ∧ watchdog_expired}`; `current - _owned` → WARN(owned:enter), `_owned - current` → INFO(owned:exit); `_owned = current`. run_once 매 tick 호출. 예외 격리.
- per-tick 스킵/no-fire 평가는 로그 X, 카운터만(30초 로그 스팸 방지). 영속 거부 반복 경고는 REQ-052 별도.
## 3. CLI 표시 (REQ-032)
- `_cmd_slots`: snapshot 응답의 `watchdog` 필드에서 `expired=true` 인 portfolio_id 집합 → 자산 출력에 `[WATCHDOG-OWNED: strategy absent]` 태그 + `watchdog_owned=N` 요약. `--json` 은 원응답 그대로.
## 4. 배선
- daemon: snapshot handler 에 `watchdog_timeout_sec=config.safety_sell.watchdog_timeout_sec`; SafetySellService 에 `instance_key=config.instance_key`.
## 5. baseline 문서 (REQ-042)
- runtime-flows: 워치독 갱신 flow 에 상태 노출(5) + `safety-sell 가시화` flow 신규(소유 진입/이탈·발동 차등·카운터·best-effort).
- interface-contract: snapshot 응답 `watchdog[]` 필드.
## best-effort 격리 (NFR-1)
- `_attach_watchdog`·`_update_ownership` 예외 무시(no-op), `_log_fire` 는 순수 로그/카운터. 워치독 판정(is_expired)·청산(`SlotService.sell`) source of truth 불변.
## 재시작 (R-3)
- WatchdogStore·`_owned`·counters in-memory → 재시작 시 소유 상태 소실(폴링 재개 시 재구성). 발동 로그는 외부 로그 싱크에 이력 유지. 영속 승격은 Out of Scope.
## 테스트 설계
- `tests/application/test_safety_sell_visibility.py`: 소유 진입 WARN+카운터, 이탈 INFO+카운터, 안정 시 churn 없음, forced_close WARN, take_profit INFO, grace 억제(6건). instance_key 로그 포함 단언.
- `tests/application/test_messaging_snapshot_consumer.py`: 워치독 만료 holding → watchdog 필드 expired/last_seen 노출.
- `tests/runtime/test_cli_operator.py`: slots 가 expired 슬롯을 WATCHDOG-OWNED 로 표시.
@@ -1,39 +0,0 @@
# BAT-021 Implementation
- Status: completed
- Batch: BAT-021
## 구현 요약
- dcy-017 워치독 소유권 가시화(snapshot 워치독 상태 + safety-sell 발동 로깅)를 전부 change-req 로 구현(신규 REQ 없음, Redis 미채택).
## 변경 내역
### REQ-034/021 — 워치독 상태 노출
- `trex_core/application/watchdog.py`: `last_seen(slot_id) -> Optional[datetime]` read-only getter.
- `trex_core/application/messaging_handlers.py`: `make_snapshot_rpc_handler(..., watchdog_timeout_sec=120)` + `_attach_watchdog(payload, ...)` — holding 슬롯 `{slot_id, portfolio_id, symbol, expired, last_seen}` 를 `payload["watchdog"]` 에 best-effort 동봉(envelope·stub 두 경로). try/except 격리.
- `trex_core/runtime/daemon.py`: handler 에 `watchdog_timeout_sec=config.safety_sell.watchdog_timeout_sec` 배선.
### REQ-037/038/039 — safety-sell 발동·소유 가시화
- `trex_core/application/safety_sell.py`:
- `SafetySellService(instance_key="default")` + `self._owned: set` + `self.counters: dict` + `_count`.
- `_log_fire(slot, reason)`: WARN(`forced_close_at`/`stop_loss`)·INFO(`take_profit`/`market_close_liquidation`) + `fire:{reason}` 카운터, 구조화 필드.
- `_update_ownership(now)`: grace 밖에서 holding∧expired 집합 diff → 진입 WARN(`owned:enter`)/이탈 INFO(`owned:exit`). 예외 격리.
- check_forced_close/check_prices/check_market_close 발동 지점에서 `_log_fire`; run_once 가 매 tick `_update_ownership`.
- `trex_core/runtime/daemon.py`: SafetySellService 에 `instance_key=config.instance_key`.
### REQ-032 — CLI 표시
- `trex_core/runtime/cli.py::_cmd_slots`: snapshot 응답 `watchdog` 에서 `expired` 슬롯 → `[WATCHDOG-OWNED: strategy absent]` 태그 + `watchdog_owned=N` 요약.
### REQ-042 — baseline 문서
- `docs/runtime-flows.md`: 워치독 갱신 flow 에 상태 노출(5) + `safety-sell 가시화` flow 신규. Last Updated → BAT-021.
- `docs/interface-contract.md`: snapshot 응답 `watchdog[]` 필드. Last Updated → BAT-021.
## 테스트
- 신규 `tests/application/test_safety_sell_visibility.py`: 6건 — 소유 진입 WARN+카운터(instance_key 포함), 이탈 INFO+카운터, 안정 시 churn 없음, forced_close WARN, take_profit INFO, grace 억제.
- `tests/application/test_messaging_snapshot_consumer.py` 확장: 워치독 만료 holding → watchdog 필드 expired/last_seen 노출(operator 비침습).
- `tests/runtime/test_cli_operator.py` 확장: slots 가 expired 슬롯 WATCHDOG-OWNED 표시.
## 검증 실행
- 단위: `TREX_BROKER=paper .venv/bin/python -m pytest -q` → `335 passed / 34 skipped`(`::test_sandbox_token_and_submit` deselect — LIVE `.env` 환경 의존, 본 batch 무관).
- 인프라 게이트: `TREX_POSTGRESQL_TEST_DSN=...(trex_core_test) TREX_RABBITMQ_TEST_URL=... pytest -q` → `369 passed`.
- `ruff check trex_core tests` → All checks passed.
@@ -1,34 +0,0 @@
# BAT-021: 워치독 소유권 가시화 — 스냅샷 워치독 상태 + safety-sell 발동 로깅 (dcy-017)
- Status: released
- Profile: standard
- Source Discovery: dcy-017
- Included REQ: 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)
## Summary
- dcy-017(전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화, issue #12)을 단일 standard batch 로 delivery 한다. 거버넌스는 dcy-017 OQ-3 결정대로 **전부 change-req, 신규 REQ 없음**.
- 핵심: snapshot RPC 응답에 holding 슬롯 워치독 상태(`expired`/`last_seen`)를 read-only 동봉(REQ-034/021)하고 `trex-core slots` 가 WATCHDOG-OWNED(전략 이탈)를 표시(REQ-032). safety-sell 의 워치독 소유 진입(WARN)/이탈(INFO)·발동(forced_close·stop_loss=WARN, take_profit·market_close=INFO)을 카운터+구조화 로그로 가시화(REQ-037/038/039). 모든 가시화는 best-effort(청산을 막지 않음). Redis projection·push 알림 미채택(OQ-1 (a)).
## Scope
- In Scope:
- snapshot RPC 응답 워치독 상태 필드(best-effort) + `WatchdogStore.last_seen` (REQ-034/021)
- `trex-core slots` 워치독 소유 표시 (REQ-032)
- safety-sell 워치독 소유 진입/이탈 + 발동(phase 차등 WARN/INFO) 로그 + in-process 카운터 (REQ-037/038/039)
- runtime-flows/interface-contract 정합 (REQ-042)
- Out of Scope:
- 워치독 판정 로직·timeout 기준 변경(가시화만)
- WatchdogStore in-memory → 영속 store 승격(정본 변경)
- Redis projection·pub/sub push 알림(OQ-1 (a) 미채택) — push 하드 요구 시 후속 분리
## 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-017). OQ-1((a) CLI/RPC 노출, Redis 미채택)·OQ-2(차등 매트릭스)·OQ-3(전부 change-req) Answered.
- dcy-016 으로 관측 함정(조회가 워치독 리셋) 이미 해소 → 비침습 on-demand 관측으로 issue #12 충족(A-1).
- best-effort 격리(NFR-1): _attach_watchdog·_update_ownership·_log_fire 모두 예외 격리, source of truth(in-memory 워치독)·청산 동작 불변.
- 메트릭은 in-process 카운터(`SafetySellService.counters`) — 기존 REQ 범위(신규 인프라 없음).
- WatchdogStore in-memory → 소유 *상태* 는 재시작 시 소실(폴링 재개 시 재구성), 발동 *로그* 는 이력 유지(R-3).
@@ -1,50 +0,0 @@
# BAT-021 Planning
- Status: confirmed
- Batch: BAT-021
- Profile: standard
## Included REQ
- REQ-034 (Interface, change-req): `WatchdogStore.last_seen` + snapshot 워치독 상태 노출.
- REQ-021 (Integration, change-req): snapshot 응답 `watchdog[]` 필드.
- REQ-037 (Integration, change-req): 워치독 소유 진입/이탈 + forced_close 발동 가시화.
- REQ-038 (Integration, change-req): stop_loss(WARN)/take_profit(INFO) 발동 가시화.
- REQ-039 (Integration, change-req): market_close 발동 가시화.
- REQ-032 (Interface, change-req): `trex-core slots` 워치독 소유 표시.
- REQ-042 (Documentation, change-req): runtime-flows/interface-contract 정합.
## Delivery Plan
1. REQ-034/021(서버): `watchdog.py` `last_seen` getter; `messaging_handlers.py` `_attach_watchdog`(best-effort) + `watchdog_timeout_sec` 파라미터; daemon 배선.
2. REQ-037/038/039(가시화): `safety_sell.py` `instance_key`/`counters`/`_log_fire`(phase 차등)/`_update_ownership`(소유 진입·이탈) + check_*·run_once 배선; daemon instance_key 배선.
3. REQ-032(CLI): `cli.py` `_cmd_slots` 가 watchdog 필드로 WATCHDOG-OWNED 표시.
4. REQ-042(문서): runtime-flows(워치독 노출 + safety-sell 가시화 flow), interface-contract(snapshot watchdog 필드).
5. 테스트: safety-sell 가시화 + snapshot watchdog 필드 + CLI 표시 + 전체 회귀 + ruff + 인프라 게이트.
## Dependencies
- Internal:
- dcy-007/BAT-010·011 safety-sell/워치독(REQ-034/037/038/039) — 가시화 대상.
- dcy-016/BAT-020 운영자 CLI `slots`·워치독 억제 읽기(REQ-032/021) — 비침습 관측 기반(A-1).
- dcy-011/dcy-013 의 WARN+카운터 가시화 패턴 정합(NFR-3).
- External:
- 없음(Redis 미채택). in-process 카운터.
## Design Gate
- Design Required Before Implementation: yes
- Reason: (a) 가시화 부수효과가 워치독 판정·safety-sell 청산을 절대 막으면 안 됨(R-1/NFR-1) — best-effort 격리 경계를 설계에서 못박아야 안전 기능 회귀를 막는다. (b) 워치독 소유 진입/이탈 diff 의 grace 처리·상태 보관(재시작 소실, R-3)·phase별 로그 레벨 매트릭스(OQ-2)를 설계에서 고정. (c) snapshot 응답에 watchdog 필드 추가가 REQ-028 strict envelope 와 충돌하지 않도록 additive top-level 필드 경계 확정. → design.md 동일 턴 scaffold.
## Milestones
1. M1 — REQ-034/021 watchdog last_seen + snapshot 필드, 핸들러 테스트 green
2. M2 — REQ-037/038/039 safety-sell 소유/발동 가시화 + 카운터, 가시화 테스트 green
3. M3 — REQ-032 CLI 표시
4. M4 — REQ-042 문서 정합
5. M5 — 전체 회귀 + ruff + 인프라 게이트
## Risks
- R-1 (High): 가시화가 실패/지연해 워치독 판정·청산을 막음. 대응: _attach_watchdog/_update_ownership/_log_fire 예외 격리, source of truth(in-memory) 불변. (NFR-1)
- R-2 (Low, OQ-1): 메커니즘 과잉. 대응: Redis 미채택, CLI/RPC 노출 + 로깅으로 한정.
- R-3 (Low): WatchdogStore in-memory → 재시작 시 소유 상태 소실. 대응: live 반영임을 문서화, 발동 로그는 이력 유지.
## Notes
- 1차 게이트: 전체 회귀 green(워치독 판정·safety-sell 트리거 무회귀 — 관측만 추가).
- 가시화 테스트는 caplog(WARN/INFO) + counters 단언으로 결정론.
- snapshot watchdog 필드는 operator(touch_watchdog=false) 요청으로 비침습 검증(폴링이 last_seen 갱신 안 함).

Some files were not shown because too many files have changed in this diff Show More