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,245 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-001_20260603_slot-pure-domain-migration
- 이슈명: trex-core 베이스 수립 및 slot 순수 도메인 이식
- 작성 시각(KST): 2026-06-03
- 마지막 갱신 시각(KST): 2026-06-03 (draft-req)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-001, REQ-002, REQ-003, REQ-004
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: 없음 (검토 초안 내용은 본 Discovery 본문과 부록 A 에 통합)
- 해석:
- bootstrap-baseline 초기화가 끝난 빈 베이스(trex-core)에서 시작하는 첫 real Discovery이며, 입력은 trex-orbit slot 도메인 코드 분석과 운영자 확정 결정(신규 베이스 + 순수 도메인만)이다.
- 이번 Discovery는 "코어 베이스 부트스트랩 + slot 순수 도메인 이식 + 단위테스트 green"의 범위와 성공 기준을 확정하고, 잔여 미결 결정(컬럼 계약 박제, OQ-2)을 드러내는 것이 핵심이다. 패키지 네임스페이스는 `trex_core.domain.slot` 으로 확정됨(OQ-1).
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- docs/discovery/ 아래 기존 Discovery 문서가 없고(첫 반복), 후보로 비교할 주기가 존재하지 않는다.
- 주제(코어 베이스 수립 + slot 순수 도메인 이식)가 기존 어떤 REQ/Batch/Release 와도 겹치지 않는 신규 문제 정의다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- trex-core 는 Python 코드 스캐폴딩이 전혀 없다 — `CLAUDE.md`, `docs/`(baseline 4종 + active index), `.stage-pilot/`(도구·템플릿·baseline.yaml) 만 존재하고 `app/`·`pyproject.toml`·`tests/` 가 없다.
- 이식 대상인 slot 도메인은 trex-orbit `app/domain/slot/`(총 ~1,150 LOC)에 있고, 그중 순수 6개 모듈(`state.py`/`models.py`/`errors.py`/`repository.py`/`service.py`/`in_memory_repository.py`)은 import 스윕 결과 **외부 의존이 0**(stdlib + 패키지 내부 참조만)으로 확인됐다.
- trex-core 의 baseline(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/data-model.md`)은 이미 slot 도메인을 planned 구조로 반영해 둔 상태이며, 실제 코드는 아직 없어 planned 와 실제 사이에 갭이 있다.
- 기대 상태
- trex-core 가 자체 Python 프로젝트(pyproject + 패키지 레이아웃 + pytest/ruff)로 부트스트랩되어 빌드/테스트가 가능하다.
- slot 순수 도메인이 trex-core 안에 storage-agnostic 코어로 이식되고, 이식한 단위테스트가 trex-core 에서 통과한다.
- baseline 문서의 planned 구조가 실제 코드와 일치하도록 갱신된다.
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- trex-core 를 베이스로 사용할 후속 제품/도메인 개발자
- 제약사항
- 운영자 확정: 신규 베이스로 시작(trex-orbit 은 병행, 이번에 수정하지 않음) + 순수 도메인만 이식.
- 순수 도메인 경계 유지: 브로커/DB 드라이버/CLI 등 인프라에 직접 의존하지 않는다.
- 6 상태 / 8 전이 / advisory-lock 직렬화 / append-only 전이 로그 / closed→idle 재진입 +1 같은 도메인 불변식을 보존한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 개발자가 trex-core 를 clone 한 뒤 `pip install -e .[dev]` 와 `pytest` 로 slot 도메인 테스트가 통과하는 깨끗한 베이스를 확보한다.
- 후속 도메인 작성자가 slot 도메인을 trex-core 의 "표준 도메인 작성 패턴(storage-agnostic + Protocol 저장소 경계)" 참조로 삼는다.
- 다음 반복에서 영속화(PostgreSQL) 구현자가 이식된 `SlotRepository` Protocol 과 보존된 컬럼 계약(본 문서 부록 A)을 기준으로 저장소 구현을 붙인다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- trex-core Python 프로젝트 부트스트랩: `pyproject.toml`(배포명 `trex-core`) + 패키지 레이아웃(`trex_core/`, `trex_core/domain/`) + pytest/ruff 설정.
- slot 순수 도메인 이식: 6개 모듈 + `__init__.py` 복사, 공개 API 확정(`__init__` docstring 을 순수 도메인 기준으로 정리, 인프라 export 없음 확인).
- 도메인 단위테스트 이식 및 green: `test_state.py`, `test_service.py`(InMemorySlotRepository 기반)을 trex-core 에서 통과.
- 컬럼 계약 가드 신설(OQ-2 결정): 도메인에 표준 필드 집합 선언(`EXPECTED_SLOT_FIELDS`)을 두고, 순수 계약 테스트(`test_contract.py`)가 `dataclasses.fields(Slot)` 의 필드 집합과 일치함을 단언한다. DB-side 드리프트 검사는 영속화 반복으로 인계.
- baseline 문서 정합: 이식 후 `docs/project-structure.md`·`docs/runtime-flows.md` 의 planned 표기를 실제 구조와 일치하도록 갱신.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규 생성: `pyproject.toml`, `trex_core/__init__.py`, `trex_core/domain/__init__.py`, `trex_core/domain/slot/*`(6 모듈 + `__init__` + `EXPECTED_SLOT_FIELDS` 선언), `tests/domain/slot/*`(이식 2종 + 신규 `test_contract.py`).
- trex-orbit 코드: 무수정(병행).
- 문서 영향:
- `docs/project-structure.md`, `docs/runtime-flows.md` 의 planned→실제 갱신.
- `docs/srs/index.md`(REQ 등록), `docs/batches/index.md`(Batch 등록) 갱신은 후속 단계에서 발생.
- 운영 영향:
- 배포/운영 절차 변경 없음(라이브러리 시드, 런타임 데몬/CLI 없음).
- slot 도메인이 trex-orbit·trex-core 두 곳에 병존하는 임시 이중 진실 발생(동기화 부담은 리스크로 추적).
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: trex-core 를 설치/테스트 가능한 Python 패키지로 부트스트랩한다(`pyproject.toml` + 패키지 레이아웃 + pytest/ruff 설정). `pip install -e .[dev]` 후 `pytest` 가 실행 가능해야 한다.
- FR-2: slot 순수 도메인 6개 모듈(`state`/`models`/`errors`/`repository`/`service`/`in_memory_repository`)과 `__init__.py` 를 `trex_core.domain.slot` 네임스페이스(OQ-1 결정)로 이식하고, 모듈 간 import 경로를 `trex_core.domain.slot.*` 로 통일한다. 공개 API(`SlotState`/`Slot`/`SlotCapacity`/`SlotTransitionRecord`/`SlotRepository`/`SlotService`/예외/`is_allowed_transition`/`SLOT_ADVISORY_LOCK_ID`/`ALLOWED_TRANSITIONS`)를 인프라 심볼 없이 노출한다.
- FR-3: 이식된 도메인 단위테스트(`tests/domain/slot/test_state.py`, `test_service.py`)가 trex-core 에서 통과한다.
- FR-4: 이식 완료 후 `docs/project-structure.md` 와 `docs/runtime-flows.md` 의 planned 표기를 실제 코드 구조와 일치하도록 갱신한다.
- FR-5: 컬럼 계약 가드(OQ-2 결정) — 도메인 패키지에 표준 필드 집합 `EXPECTED_SLOT_FIELDS`(부록 A 를 가리키는 주석 포함)를 선언하고, 순수 계약 테스트 `tests/domain/slot/test_contract.py` 가 `{f.name for f in dataclasses.fields(Slot)} == EXPECTED_SLOT_FIELDS` 를 단언해 필드 추가/삭제 시 실패하도록 한다. 타입 단언은 하지 않고(이름/존재 중심) 타입 매핑은 부록 A 문서에 둔다. DB-side(information_schema) 드리프트 검사는 영속화 REQ 로 인계한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 순수 도메인 경계 유지 — 이식된 도메인 코드는 stdlib 와 패키지 내부 참조 외 외부 의존(psycopg/네트워크/CLI/config)을 import 하지 않는다(정적 import 스윕으로 검증 가능).
- NFR-2: 도메인 불변식 보존 — 6 상태/8 전이, advisory-lock 직렬화, append-only 전이 로그, closed→idle 재진입 +1, lock 보유 중 외부 I/O 금지가 이식 후에도 동일하게 성립한다.
- NFR-3: 컬럼 계약 보존 — `Slot` 데이터클래스 필드 계약(본 문서 부록 A)이 변형 없이 유지되어, 다음 반복의 영속화 구현이 동일 계약을 기준으로 삼을 수 있다. 보존은 선언(문서: 부록 A + `docs/data-model.md`)이 아니라 자동 가드(FR-5 계약 테스트)로 강제한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- trex-core 부트스트랩(pyproject/레이아웃/pytest/ruff).
- slot 순수 도메인 6개 모듈 + `__init__` 이식 및 공개 API 확정.
- 도메인 단위테스트(`test_state.py`, `test_service.py`) 이식 및 green.
- baseline 문서(project-structure, runtime-flows) planned→실제 갱신.
- Out of Scope
- PostgreSQL 구현체(`postgresql_repository.py`), SQL DDL/마이그레이션, `db init` 러너, schema drift 감지(`schema.py`).
- `trex slot` CLI(resolve/status/provision/archive), config 해석, coordinator 연동, daily_slot_archive/retention prune.
- trex-orbit 수정 및 trex-orbit↔trex-core 코드 수렴.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Medium): 영속화 부재로 trex-core 의 `SlotService` 는 in-memory repo 로만 구동되어, 단일 writer/advisory lock 의 실제 가치가 다음 반복(Postgres 재구성)까지 미실현된다.
- 완화 방안: 이번 반복 목표를 "도메인 코어 시드 + 테스트 green"으로 한정하고, 영속화는 별도 후속 Discovery 로 명시 인계한다.
- R-2 (Medium): DDL 을 함께 가져오지 않으므로 향후 영속화 구현 시 `Slot` 필드와 스키마가 어긋나는 컬럼 계약 드리프트 위험이 있다.
- 완화 방안: 이번 반복은 계약 테스트(FR-5)로 `Slot` 필드↔`EXPECTED_SLOT_FIELDS` 일치를 자동 강제하고, 부록 A·`docs/data-model.md` 를 문서 계약으로 둔다. DB-side(information_schema) 드리프트 검사는 영속화 REQ 로 인계한다.
- R-3 (Low): slot 도메인이 trex-orbit·trex-core 두 곳에 병존(이중 진실)해 로직 변경 시 동기화 부담이 생긴다.
- 완화 방안: 신규 베이스 결정의 의도된 비용으로 수용하고, 수렴 시점은 별도 Discovery(OQ 처리)로 추적한다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: trex-orbit 의 순수 6개 모듈은 외부 의존이 0이라 코드 수정 없이 복사 가능하다(import 스윕으로 확인됨).
- A-2: 이식 시 유일한 코드 수정은 `__init__.py` docstring 정리 수준이며, 로직 변경은 없다.
- A-3: trex-core 의 툴체인은 trex-orbit 과 동등(Python >=3.12, setuptools, pytest, ruff)을 기본 채택한다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 패키지 네임스페이스를 trex-orbit 과 동일한 `app.domain.slot` 으로 유지할지, trex-core 전용 `trex_core.domain.slot` 으로 출발할지.
- 상태: Answered (2026-06-03)
- 결정: `trex_core.domain.slot` 으로 출발(독립 베이스 네임스페이스). 근거: 재사용 코어 정체성·`app` top-level 패키지 충돌 회피·OQ-3(향후 수렴) 시 자연스러운 import 경로. 이식 시 import 라인 1회 기계적 치환 비용만 발생.
- 반영 위치: `# 4`(코드 영향 신규 생성 경로), `# 5` FR-1(레이아웃)·FR-2(import 경로 통일). baseline 구조 표기(`docs/project-structure.md`·`docs/runtime-flows.md` 등)의 `app/` planned 표기는 FR-4 갱신 시 `trex_core/` 로 정정한다.
- OQ-2: `Slot` 컬럼 계약(본 문서 부록 A)을 영속화 미구현 상태에서 어떻게 드리프트 없이 박제할지(문서 고정 vs 계약 테스트 vs 스키마 placeholder).
- 상태: Answered (2026-06-03)
- 결정: 계약 테스트를 주 수단으로 채택(2층 구조). ① 코드 가드 = 도메인의 `EXPECTED_SLOT_FIELDS` ↔ `dataclasses.fields(Slot)` 일치 단언(FR-5). ② 문서 거울 = 부록 A + `docs/data-model.md`. DB-side(information_schema) 드리프트 검사는 영속화 반복으로 인계. 근거: 문서 고정은 강제력이 없어 R-2 미완화, 스키마 placeholder 는 제외한 영속화 인프라의 조기 유입이라 범위 위반. 계약 테스트만 순수 도메인 범위 안에서 NFR-3 을 자동 강제.
- 반영 위치: `# 4`(계약 가드 신설 항목·신규 파일), `# 5` FR-5, NFR-3, `# 6` R-2 완화, `# 7` S-4.
- OQ-3: trex-orbit↔trex-core slot 도메인의 향후 수렴(역참조/단일화) 여부.
- 상태: Deferred
- 처리 방안: 이번 반복 비범위. 영속화 재구성 이후 별도 Discovery 에서 다룬다. OQ-1 결과(`trex_core.domain.slot`)를 인계 — 수렴 시 trex-orbit 이 `trex_core.domain.slot` 을 역참조하는 경로가 자연스럽다.
- 종료 조건: 영속화 반복 종료 후 수렴 Discovery 가 생성되면 그 문서로 이관.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: trex-core 가 부트스트랩되어 `pip install -e .[dev]` 후 `pytest` 가 정상 수집/실행된다.
- S-2: 이식한 slot 도메인 단위테스트 2개 파일이 모두 통과한다.
- S-3: 이식된 도메인 코드의 import 스윕에서 외부(인프라) 의존이 0으로 확인되고, baseline 문서(project-structure/runtime-flows)가 실제 구조와 일치한다.
- S-4: 계약 테스트(`test_contract.py`)가 `Slot` 필드 집합과 `EXPECTED_SLOT_FIELDS` 일치를 단언하고 통과한다(필드 변경 시 실패하는 것을 음성 케이스로 확인).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 클린 환경에서 설치 후 `pytest` 실행 로그 확인 / 데이터 출처: CI 또는 로컬 `pytest` 출력, `pyproject.toml`.
- S-2 측정 방식: `pytest tests/domain/slot` 통과 여부 / 데이터 출처: 테스트 실행 결과(green count).
- S-3 측정 방식: `grep`/정적 import 스윕으로 외부 모듈 import 0 확인 + baseline 문서와 실제 트리 비교 / 데이터 출처: 소스 import 목록, `docs/project-structure.md`·`docs/runtime-flows.md`.
- S-4 측정 방식: `pytest tests/domain/slot/test_contract.py` 통과 + 임시 필드 추가/삭제 시 실패 재현 / 데이터 출처: 계약 테스트 실행 결과, `EXPECTED_SLOT_FIELDS` 선언.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 운영자가 In/Out 경계(특히 영속화·CLI 제외)를 확인했다. → confirm-discovery 에서 문서대로 확정(2026-06-03).
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3 각각 측정 방식과 데이터 출처가 명시됨(본 문서 # 7).
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: High 등급 리스크 없음(R-1/R-2 Medium, R-3 Low) — 별도 책임자 지정 불요, 완화 방안 명시됨.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료 (OQ-1·OQ-2 Answered, OQ-3 Deferred → `Open` 0개)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다. → Confirmed By: Justin.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-001_20260603_slot-pure-domain-migration.md
- 참조 문서:
- docs/project-structure.md, docs/runtime-flows.md (구조/런타임 baseline — 본 Discovery 가 갱신 요구)
- docs/interface-contract.md, docs/data-model.md (인터페이스/데이터 baseline — 컬럼 계약 기준)
- stage-pilot/templates/project-structure.md, stage-pilot/templates/runtime-flows.md (baseline 갱신 시 논리 템플릿 참조)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~패키지 네임스페이스~~ → 결정됨: `trex_core.domain.slot` (OQ-1, 2026-06-03)
- ~~컬럼 계약 박제 수단~~ → 결정됨: 계약 테스트 주 수단 + 문서 거울, DB-side 드리프트는 영속화 반복 인계 (OQ-2, 2026-06-03)
## CONFIRM
- ~~범위 경계~~ → 확정됨: 문서 §5 범위 경계대로(영속화·SQL·CLI·config·coordinator·trex-orbit 수정 = Out of Scope) (confirm-discovery, 2026-06-03)
- ~~툴체인~~ → 확정됨: trex-orbit 동등(Python >=3.12 + setuptools + pytest + ruff) 채택 (confirm-discovery, 2026-06-03)
## DATA
- 없음 (별도 외부 데이터 요청 없음)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: 모든 오픈 질문 해소(OQ-1 `trex_core.domain.slot`·OQ-2 계약 테스트 Answered, OQ-3 Deferred). 범위·툴체인 CONFIRM(§10)을 confirm-discovery 에서 확정하고 CONFIRMED_BY 지정 완료. REQ Drafting(FR-1~FR-5) 으로 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-03
# 부록 A. `Slot` 데이터클래스 ↔ `daily_slot` 컬럼 계약 (이식 보존 대상)
NFR-3 과 OQ-2 가 참조하는 컬럼 계약이다. 순수 도메인의 `Slot` 데이터클래스는 trex-orbit `daily_slot` 스키마 컬럼 계약을 암묵 전제하므로, trex-core 가 다음 반복에서 영속화 계층을 새로 구성할 때 이 계약을 반드시 만족시켜야 도메인이 정상 동작한다. (`docs/data-model.md` 와 동일 기준)
| 필드 | 타입(도메인) | 원본 DDL 타입 | 비고 |
|---|---|---|---|
| `slot_id` | UUID | UUID PK | |
| `trading_date` | date | DATE NOT NULL | |
| `status` | SlotState | TEXT CHECK(6값) | idle/pending_buy/holding/pending_sell/closed/error |
| `symbol` | str? | TEXT | |
| `qty` | float? | NUMERIC(18,4) | |
| `avg_price` | float? | NUMERIC(18,4) | |
| `entry_price` | float? | NUMERIC(18,4) | 가격 잠금 |
| `allocated_capital` | float? | NUMERIC(18,2) | |
| `order_id` | str? | TEXT | 자유 주석 |
| `strategy_id` | str? | TEXT | 진입 전략 |
| `broker_order_id` | str? | TEXT | 브로커 식별자 |
| `portfolio_id` | str? | TEXT | |
| `sell_strategy_id` | str? | TEXT | 청산 전략 |
| `forced_close_at` | datetime? | TIMESTAMPTZ | 강제청산 마감선 |
| `created_at` / `updated_at` | datetime? | TIMESTAMPTZ | |
부속 테이블 계약(영속화 재구성 시 함께 필요): `daily_symbol_entries`(재진입 카운터 — PK `(trading_date, symbol)`), `slot_transition_log`(append-only 감사 로그). 본 이관(순수 도메인)에서는 계약만 보존하고 DDL 은 가져오지 않는다.
@@ -1,238 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed (transport 부분은 dcy-006/BAT-007 에서 RabbitMQ 로 대체됨; logical intent/admission 계약은 유효)
- 문서 ID: dcy-002_20260603_slot-core-daemon-http-api
- 이슈명: trex-core 슬롯 코어 데몬화 + HTTP intent API
- 작성 시각(KST): 2026-06-03
- 마지막 갱신 시각(KST): 2026-06-05 (BAT-007 transport supersede)
- 대체됨: 본 Discovery 의 **HTTP `/v1` API transport** 가정은 dcy-006(`strategy-module-rabbitmq-transport`, BAT-007)에서 RabbitMQ messaging 으로 대체됨. logical 계약(intent admission, idempotency via `intent_log`, 단일 writer 불변식)은 그대로 유효하며 transport 만 교체. `adapters/api/` HTTP 어댑터, `GET /v1/healthz`, `POST /v1/intents/buy|sell`, `GET /v1/slots`, `GET /v1/capacity` 엔드포인트는 BAT-007 에서 제거됨.
- 후속 Discovery 참조: dcy-006_20260605_strategy-module-rabbitmq-transport (transport 대체)
- 생성된 REQ 참조: REQ-005, REQ-006, REQ-007, REQ-008, REQ-009
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: trex-orbit `orbit-v2_plan.md`(3-데몬 분리 설계안 — 슬롯 코어/매수·매도 결정 producer/ intent 핸드오프 사전 설계)
- 해석:
- dcy-001/BAT-001 로 trex-core 에 slot 순수 도메인(in-memory, 라이브러리)이 시드됐다. 이번 Discovery 는 그 코어를 **장기 실행 데몬**으로 승격하고, 매수·매도 전략이 **HTTP API 엔드포인트**로 intent 를 제출/조회하는 경계를 정의한다.
- 운영자 결정: 전략↔코어 경계는 **네트워크 HTTP API**다(DB-only outbox 가 아니라 API 엔드포인트). 순수 도메인은 안쪽 코어로 유지하고 데몬·API·영속화를 바깥 레이어로 추가한다(onion).
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-001 은 "순수 도메인 이식(라이브러리)"이 문제 정의였고, 이번은 "데몬화 + API 경계 + 영속화"로 범위·구조가 다르다.
- dcy-001 이 명시적으로 Out of Scope 로 둔 영속화/CLI/런타임을 정면으로 다루므로 기존 Discovery 갱신이 아니라 후속 독립 주기가 맞다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- trex-core 는 `trex_core/domain/slot/` 순수 도메인만 보유(상태탐색 결과: server/api/daemon/http/psycopg 참조 0, console_scripts 0). `SlotService` 는 in-memory repo 로만 구동되고 장기 실행 프로세스·외부 호출 경계가 없다.
- 영속화 구현체(PostgreSQL `SlotRepository`)·브로커·API·데몬 런타임이 모두 부재(dcy-001 에서 의도적으로 범위 밖이었음, dcy-001 R-1).
- 설계 참고자료(`orbit-v2_plan.md`)는 이미 슬롯 코어 단일 writer(불변식 A)·단일 브로커 세션(불변식 B)·buy/sell intent 핸드오프·safety floor 를 정리해 둔 상태다.
- 기대 상태
- trex-core 가 데몬 프로세스로 떠서 slot 도메인을 **durable 단일 writer**로 소유한다.
- 매수·매도 전략 데몬이 HTTP API 로 intent 를 제출하고(매수/매도), holding 슬롯 스냅샷을 read-only 로 조회한다.
- 코어가 intent 를 심사(admission)해 슬롯 전이를 단독 수행한다(전략은 "제안", 코어가 "집행").
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- 코어 API 소비자: 매수·매도 전략 데몬(별도 프로세스)
- 제약사항
- 불변식 A — `daily_slot` 단일 writer 는 코어뿐. 전략은 슬롯을 직접 쓰지 않는다.
- 순수 도메인 경계 유지 — `trex_core.domain.slot` 은 웹 프레임워크/DB 드라이버에 의존하지 않는다(의존은 API·persistence 어댑터 레이어에만).
- dcy-001 부록 A 의 `Slot` 컬럼 계약을 영속화 구현이 만족해야 한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 매수 전략 데몬이 신호 발생 시 `POST /v1/intents/buy`(symbol/qty/...)를 호출하면, 코어가 admission 통과 시 idle→pending_buy 로 전이하고 결과를 응답한다.
- 매도 전략 데몬이 `GET /v1/slots?status=holding` 로 보유 슬롯 스냅샷을 읽어 전략을 평가한 뒤 `POST /v1/intents/sell` 로 청산 intent 를 제출한다.
- 운영자가 `GET /v1/capacity`·`GET /v1/healthz` 로 코어 데몬의 슬롯 가용성과 생존을 모니터링한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 데몬 런타임: trex-core 를 장기 실행 프로세스로 기동하는 엔트리포인트 + lifecycle(start/shutdown/healthz).
- HTTP API 경계: intent 제출(buy/sell) + holding 슬롯 read-only 스냅샷 + capacity/health 엔드포인트.
- 영속화: `SlotRepository` 의 PostgreSQL 구현(단일 writer, advisory lock 실제화) + 스키마/마이그레이션. in-memory 는 테스트용으로 잔존.
- 멱등 intent 처리(OQ-1 결정): `intent_id` 기반 dedup + 수락 시 **동기** 슬롯 전이 커밋(불변식 A 강제). 수락된 intent 는 같은 트랜잭션에서 `intent_log` 에 durable 기록(dedup 저장소 + audit + 향후 outbox 전구체). 완전 비동기 outbox 는 브로커 집행 Discovery 로 Deferred.
- baseline 갱신: `docs/project-structure.md`·`docs/runtime-flows.md`·`docs/interface-contract.md` 에 데몬/API/영속화 레이어 반영.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규 레이어: `trex_core/` 하위 API 어댑터(FastAPI+uvicorn, OQ-2 결정), persistence 어댑터(psycopg), 데몬 런타임/엔트리포인트.
- `pyproject.toml`: FastAPI+uvicorn + psycopg 의존성 및 console_scripts 추가. 도메인 패키지는 무의존 유지.
- 문서 영향:
- `docs/interface-contract.md` 에 HTTP API 계약 추가, `docs/runtime-flows.md` 에 데몬/요청 흐름 추가, `docs/data-model.md`/`docs/project-structure.md` 갱신.
- 운영 영향:
- 신규 운영물: 장기 실행 데몬 프로세스 + PostgreSQL 의존 + API 포트/헬스체크. 배포·모니터링 절차 신설.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: trex-core 를 데몬으로 기동하는 엔트리포인트와 lifecycle(기동/정상종료/`GET /v1/healthz`)을 제공한다. 거래일 슬롯 사전할당(preallocate)은 공개 엔드포인트가 아니라 데몬 lifecycle 내부(기동/스케줄) 책임으로 둔다(OQ-3 결정).
- FR-2: HTTP API 로 매수 intent(`POST /v1/intents/buy`)와 매도 intent(`POST /v1/intents/sell`)를 수락하고, 코어가 admission/검증 후 슬롯 전이(idle→pending_buy / holding→pending_sell)를 단독 수행한다. API 는 `/v1` 버전 프리픽스를 사용한다(OQ-3 결정).
- FR-3: HTTP API 로 holding 슬롯 스냅샷(`GET /v1/slots?status=holding`)과 capacity(`GET /v1/capacity`)를 read-only 로 제공한다.
- FR-4: intent 는 `intent_id` 멱등키로 dedup 하여 동일 intent 재전송이 슬롯을 중복 전이시키지 않는다. 수락 시 admission+슬롯 전이를 **동기**로 커밋해 호출자에게 admit/reject 를 응답하고(OQ-1 결정), 수락된 intent 를 `intent_log`(intent_id, payload, decision, ts) 에 같은 트랜잭션으로 durable 기록한다. `GET /v1/intents/{intent_id}` 로 결정을 재조회할 수 있어(응답 유실 시) 멱등 재시도를 지원한다. 완전 비동기 outbox 는 브로커 Discovery 로 Deferred.
- FR-5: `SlotRepository` 의 PostgreSQL 구현을 추가해 코어가 `daily_slot` 단일 writer 로 durable 하게 동작한다(advisory lock 실제화). 스키마는 **trex-core 소유의 신규 마이그레이션**으로 작성하되 dcy-001 부록 A 를 단일 기준으로 하고 orbit-v2 DDL 을 참조 구현으로만 쓴다(OQ-4 결정). 슬라이스 1 테이블: `daily_slot`(부록 A 16컬럼)·`daily_symbol_entries`·`slot_transition_log` + `intent_log`(OQ-1). `daily_slot_archive`/prune 는 범위 밖. `EXPECTED_SLOT_FIELDS` ↔ `daily_slot` 실제 컬럼(information_schema) 드리프트 테스트를 동반해 dcy-001 OQ-2 가 인계한 DB-side 검사를 실현한다.
- FR-6: `docs/interface-contract.md`·`docs/runtime-flows.md`·`docs/project-structure.md`·`docs/data-model.md` 를 데몬/API/영속화 레이어에 맞게 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 도메인 순수성 유지 — `trex_core.domain.slot` 은 웹 프레임워크/psycopg 에 의존하지 않는다(의존은 API·persistence 어댑터 레이어에 격리; import 스윕으로 검증).
- NFR-2: 단일 writer 불변식 — `daily_slot`/`SlotService.transit_slot` 쓰기는 코어 경로에서만 일어난다(API 핸들러가 advisory lock 으로 직렬화).
- NFR-3: 멱등/내구성 — 수락된 intent 의 슬롯 전이는 at-least-once 재전송에도 1회만 반영된다(`intent_id` dedup via `intent_log`). "수락 == 커밋"이라 수락-후-미처리 유실 창이 없다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 데몬 런타임 + HTTP API(intent 제출 + holding/capacity 조회 + health).
- PostgreSQL `SlotRepository` 영속화(단일 writer) + 멱등 intent 처리.
- baseline 문서 갱신.
- Out of Scope (후속 Discovery)
- 완전 비동기 durable outbox(intent 상태머신 + 소비 루프) — 브로커 집행 Discovery 에서 `intent_log` 를 승격(OQ-1 Deferred 부분).
- 브로커 단일 세션/실제 주문 집행(불변식 B, order execution).
- safety floor(forced_close/EOD/손절 backstop, liveness 게이팅) 및 sell_evaluation_log.
- 매수·매도 전략 데몬 자체 구현(코어의 소비자이며 별 repo/프로세스).
- 인증/인가 고도화, 멀티테넌시, 수평 확장.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 브로커 집행 없이 코어만 만들면 intent→슬롯 전이까지만 동작해 "거래"는 미완성. 부분 가치만 실현.
- 완화 방안: 이번 반복을 "durable 단일 writer 슬롯 코어 + API 경계"로 한정하고, 브로커/ safety floor 를 후속 Discovery 로 명시 인계(범위 경계에 박음).
- R-2 (Medium): HTTP 는 at-least-once 를 보장하지 않아 intent 중복/유실 가능.
- 완화 방안(OQ-1 결정): `intent_id` 멱등키(FR-4) + 수락 시 동기 커밋 + `intent_log` durable 기록. 브로커 집행이 들어오는 후속 Discovery 에서 `intent_log` 에 status+소비 루프를 얹어 outbox 로 점진 승격(API 계약 무변경).
- R-3 (Medium): 웹 프레임워크/psycopg 의존이 도메인 순수성을 오염시킬 위험.
- 완화 방안: 레이어 분리(domain/ application/ adapters) + import 스윕 가드(NFR-1, dcy-001 의 계약 테스트 패턴 확장).
- R-4 (Medium): orbit-v2 SQL 스키마와 trex-core `Slot` 컬럼 계약(부록 A) 불일치 위험.
- 완화 방안: 부록 A 를 기준으로 스키마 작성 + 드리프트 테스트(dcy-001 이 인계한 DB-side 검사 여기서 실현).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 매수·매도 전략은 별도 프로세스이며 코어 API 의 클라이언트다(코어는 전략 로직을 포함하지 않는다).
- A-2: 단일 코어 인스턴스가 `daily_slot` 단일 writer 다(슬라이스 1 은 수평 확장 비대상).
- A-3: 영속화는 PostgreSQL 을 사용하며 dcy-001 부록 A 컬럼 계약을 만족한다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: intent 내구성 — 수락 시 동기 admission+커밋(슬라이스 1 기본) vs 내부 durable outbox 적재 후 비동기 소비.
- 상태: Answered (2026-06-03)
- 결정: **동기 커밋 + `intent_id` 멱등 + durable `intent_log` 이음매**. 수락 시 admission+슬롯 전이를 동기 커밋하고 admit/reject 를 즉시 응답, 수락 intent 를 같은 트랜잭션에서 `intent_log` 에 기록. 완전 비동기 outbox 는 가치(느린 브로커 집행 디커플링)가 실현되는 **브로커 집행 Discovery 로 Deferred** 하며, 그때 `intent_log` 에 status+소비 루프를 얹어 API 계약 변경 없이 승격한다. 근거: 이번 범위엔 intent 뒤 느린 다운스트림이 없어 outbox 이득 미실현 + 동기 응답이 매수 admit/reject UX 에 유리 + "수락==커밋"이 유실 창 제거.
- 반영 위치: `# 4`(멱등 처리 항목), `# 5` FR-4·NFR-3, `# 6` R-2 완화, `# 5` Out of Scope(완전 outbox Deferred).
- OQ-2: API 프레임워크/스택 — FastAPI+uvicorn(기본 권장) vs Flask vs stdlib http. Python >=3.12.
- 상태: Answered (2026-06-03)
- 결정: **FastAPI + uvicorn** 채택. 근거: 타입 기반 요청/응답 검증(pydantic), async, OpenAPI 자동 문서. Python >=3.12.
- 반영 위치: `# 4` 코드 영향(API 어댑터·pyproject 의존성). REQ 단계에서 부트스트랩(REQ-A 계열)에 의존성·버전 고정.
- OQ-3: 엔드포인트 1차 범위 — `/intents/buy`,`/intents/sell`,`/slots`,`/capacity`,`/healthz` 로 충분한지, admission 결과 조회/취소 엔드포인트가 필요한지.
- 상태: Answered (2026-06-03)
- 결정: 아래 6개로 1차 고정(`/v1` 버전 프리픽스). 모두 단일 코어 데몬이 노출.
- `GET /v1/healthz` — liveness/readiness.
- `POST /v1/intents/buy` — 매수 intent 수락, 동기 admit/reject 응답(+ slot_id).
- `POST /v1/intents/sell` — 매도 intent 수락, 동기 admit/reject 응답.
- `GET /v1/intents/{intent_id}` — 멱등 재조회/audit(POST 응답 유실 시 결정 재확인; FR-4 보강).
- `GET /v1/slots?status=holding` — holding 슬롯 스냅샷 read-only(status/trading_date 필터).
- `GET /v1/capacity` — idle/holding/pending + scarcity_ratio.
- 제외(슬라이스 1): intent 취소/admission 취소 엔드포인트(브로커·rollback 경로와 함께 후속), 슬롯 사전할당 엔드포인트(데몬 lifecycle 내부 처리, FR-1).
- 반영 위치: `# 5` FR-1/FR-2/FR-3/FR-4, `# 4` HTTP API 경계. interface-contract 초안은 REQ/Batch design 에서 고정.
- OQ-4: 영속화 스키마 출처 — orbit-v2 의 slot SQL 을 가져와 정합 vs 부록 A 기준으로 신규 작성.
- 상태: Answered (2026-06-03)
- 결정: **부록 A 를 단일 기준으로 trex-core 소유의 신규 마이그레이션을 작성**하고 orbit-v2 DDL 은 참조 구현으로만 사용. 슬라이스 1 테이블 = `daily_slot`·`daily_symbol_entries`·`slot_transition_log`·`intent_log`(archive/prune 제외). `EXPECTED_SLOT_FIELDS`↔information_schema 드리프트 테스트로 dcy-001 OQ-2 의 DB-side 검사 실현. 근거: 신규 베이스가 자기 스키마 lineage 를 소유(OQ-1 독립 결정과 일관) + 검증된 orbit DDL 재사용(재발명 회피).
- 반영 위치: `# 5` FR-5, `# 6` R-4 완화.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 코어 데몬을 기동하고 `GET /v1/healthz` 가 200 을 반환한다.
- S-2: `POST /v1/intents/buy` 호출 시 idle 슬롯이 pending_buy 로 전이되고, 동일 `intent_id` 재전송이 중복 전이를 만들지 않는다(멱등).
- S-3: `GET /v1/slots?status=holding` 가 holding 슬롯 스냅샷을 read-only 로 반환한다.
- S-4: PostgreSQL repo 로 코어를 구동했을 때 도메인 단위테스트/통합테스트가 통과하고, `daily_slot` 쓰기가 코어 경로로만 발생한다(단일 writer).
- S-5: 도메인 패키지 import 스윕에서 웹 프레임워크/psycopg 의존이 0 으로 유지된다(레이어 격리).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 데몬 기동 후 healthz 호출 / 데이터 출처: 통합테스트 또는 수동 curl 로그.
- S-2 측정 방식: API 통합테스트(정상 1회 + 중복 재전송) / 데이터 출처: 테스트 결과, 슬롯 상태·transition_log.
- S-3 측정 방식: holding 슬롯 조성 후 GET 응답 검증 / 데이터 출처: API 통합테스트.
- S-4 측정 방식: PostgreSQL 백엔드 통합테스트(가능 시) + 쓰기 경로 정적 검토 / 데이터 출처: 테스트 결과, 코드 리뷰.
- S-5 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain/slot` import 목록.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — 특히 브로커/safety floor 제외
- 기준: 운영자가 슬라이스 1 경계를 확인했다. → confirm-discovery 에서 문서대로 확정(2026-06-03).
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 측정 방식·출처 명시(본 문서 # 7).
- [x] High 영향 리스크(R-1) 처리 방침 확정
- 기준: R-1(브로커 미포함) 을 후속 Discovery 인계로 수용. → 범위 경계 확정과 함께 수용(2026-06-03).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 0개 또는 잔여 전부 `Deferred` (OQ-1~OQ-4 전부 Answered → `Open` 0개)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다. → Confirmed By: Justin.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-002_20260603_slot-core-daemon-http-api.md
- 참조 문서:
- docs/discovery/dcy-001_20260603_slot-pure-domain-migration.md (순수 도메인 코어 + 부록 A 컬럼 계약)
- docs/project-structure.md, docs/runtime-flows.md, docs/interface-contract.md, docs/data-model.md (baseline — 본 Discovery 가 갱신 요구)
- trex-orbit `orbit-v2_plan.md` (3-데몬 설계 참고자료)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~intent 내구성~~ → 결정됨: 동기 커밋 + `intent_id` 멱등 + durable `intent_log`, 완전 outbox 는 브로커 Discovery 로 Deferred (OQ-1, 2026-06-03)
- ~~영속화 스키마 출처~~ → 결정됨: 부록 A 기준 trex-core 소유 신규 마이그레이션(orbit-v2 DDL 참조), archive 제외, 드리프트 테스트 동반 (OQ-4, 2026-06-03)
## CONFIRM
- ~~API 프레임워크~~ → 결정됨: FastAPI + uvicorn (OQ-2, 2026-06-03)
- ~~1차 엔드포인트 집합~~ → 결정됨: `/v1`(healthz, intents/buy, intents/sell, intents/{id}, slots, capacity); 취소·preallocate 엔드포인트 제외 (OQ-3, 2026-06-03)
- ~~범위 경계(브로커 deferral)~~ → 확정됨: 브로커 집행·safety floor·완전 outbox·전략 데몬을 후속 Discovery 로 미루고 슬라이스 1(데몬+API+영속화+멱등) 승인 (confirm-discovery, 2026-06-03)
## DATA
- 없음 (별도 외부 데이터 요청 없음)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~OQ-4 전부 Answered(내구성=동기+intent_log / 프레임워크=FastAPI / 엔드포인트=/v1 6종 / 스키마=부록 A 기준 신규). 범위 경계(브로커·safety floor·outbox·전략 데몬 후속 Deferred)와 R-1 처리 방침을 confirm-discovery 에서 확정, CONFIRMED_BY 지정 완료. REQ Drafting 으로 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-03
@@ -1,232 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-003_20260604_broker-execution-and-outbox
- 이슈명: 브로커 단일 세션 + 주문 집행 + durable outbox (intent→실제 주문, 불변식 B)
- 작성 시각(KST): 2026-06-04
- 마지막 갱신 시각(KST): 2026-06-04 (draft-req)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-011, REQ-012, REQ-013, REQ-014, REQ-015
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: trex-orbit `orbit-v2_plan.md`(불변식 B·Order Execution·forced_close 소비자 사전 설계), dcy-002(슬롯 코어 데몬·intent API, 브로커/outbox 를 본 Discovery 로 Deferred)
- 해석:
- dcy-002/BAT-002 로 코어가 intent 를 받아 슬롯을 `pending_buy`/`pending_sell` 로 전이까지는 하지만, **실제 브로커 주문이 없어 pending 슬롯이 체결로 진행되지 않는다**(거래 미완성, dcy-002 R-1).
- 이번 Discovery 는 코어에 **단일 브로커 세션(불변식 B)** + **주문 집행/체결 처리** + **durable 주문 핸드오프(outbox 승격)**를 더해 intent 를 실제 주문으로 잇는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-002 는 명시적으로 브로커 집행·완전 outbox 를 Out of Scope 로 두고 본 후속 Discovery 로 Deferred 했다.
- 기존 REQ 본문 수정이 아니라 신규 능력(브로커 세션·주문 집행·체결·outbox)이라 `change-req` 가 아니라 새 Discovery 다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 코어 데몬은 intent 수락 → 슬롯 `idle→pending_buy`/`holding→pending_sell` 전이까지 수행(단일 writer, 불변식 A). 그러나 브로커/주문 집행/체결 처리 레이어가 없다(상태탐색: broker/order-exec/fill/dispatch 모듈 0).
- `pending_buy`/`pending_sell` 슬롯을 `holding`/`closed` 로 진행시키는 **시스템 경로가 없다**(전이는 허용 정의만 있고 구동 주체 부재 — 현재는 테스트만 수동 전이).
- `intent_log` 는 dedup 게이트 전용(비동기 outbox 아님, dcy-002 OQ-1).
- 기대 상태
- 코어가 **단일 브로커 세션**을 소유(불변식 B)하고, pending 슬롯을 집행해 매수/매도 주문을 송신한다.
- 체결 시 `pending_buy→holding`(매수 체결) / `pending_sell→closed`(매도 체결), 거부/취소 시 `pending_buy→idle` 롤백을 코어가 단독 수행.
- 주문 핸드오프가 durable 하여 크래시 후 reconciliation 가능(중복 주문 없이 재개).
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- 코어 API 소비자(매수·매도 전략 데몬) — 집행 결과(체결/거부)를 슬롯 상태로 관측
- 제약사항
- 불변식 A — `daily_slot` 단일 writer 는 코어뿐(유지).
- 불변식 B — 브로커 주문/취소는 코어의 단일 세션만(전략은 브로커 직접 접근 금지).
- 도메인 순수성 — `trex_core.domain.slot` 은 브로커/네트워크 의존 0 유지(브로커는 adapters 레이어).
- 실거래 안전 — 중복 주문 금지(멱등), 크래시 reconciliation, 안전한 기본값.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 매수 intent 가 `pending_buy` 로 전이된 뒤, 코어 집행이 브로커에 매수 주문을 송신하고 체결되면 슬롯이 `holding` 으로 진행된다.
- 매도 intent 로 `pending_sell` 이 된 슬롯을 코어가 매도 주문 송신 → 체결 시 `closed` 로 진행, 재진입 카운트 +1(recovery).
- 데몬이 주문 송신 직후 크래시 → 재기동 시 reconciliation 으로 미체결/중복 주문을 정리(중복 주문 없음).
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 브로커 포트 + 단일 세션 어댑터(불변식 B): `BrokerPort`(submit_order/cancel/poll_fill 등) + 어댑터(슬라이스는 paper/stub 브로커 우선).
- 주문 집행 루프(OQ-4 결정=별도 백그라운드 루프): pending 슬롯 claim → 주문 송신 → `broker_order_id` 기록 → 체결 폴링/처리 → 슬롯 전이(holding/closed) + 거부 시 롤백(idle) + reconcile. intent API 는 pending 전이까지만, 집행은 루프가 비동기.
- durable 주문 핸드오프(OQ-3 결정=옵션 A): pending 슬롯 자체가 실행 큐. `broker_order_id` 멱등 가드 + 재기동 reconciliation. 별도 outbox 테이블 미신설(부분체결/재시도 본격화 시 옵션 B 승격 hook).
- baseline 갱신: runtime-flows(집행/체결 흐름)·interface-contract(브로커 경계)·project-structure(broker adapter/execution 레이어)·data-model(주문/체결 상태).
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규: `trex_core/` 하위 broker 포트 + 어댑터 2종(paper/stub + **KIS**), 주문 집행 컴포넌트(루프/워커), 체결 폴링/처리, reconciliation. `pyproject` 에 KIS 호출용 HTTP 의존(httpx 등) + KIS 자격증명 config(.env).
- 기존: 집행이 SlotService 로 `pending→holding/closed`/`pending_buy→idle` 전이를 구동(단일 writer 유지).
- 문서 영향:
- runtime-flows/interface-contract/project-structure/data-model 갱신.
- 운영 영향:
- 실주문 송신 → 실거래 리스크. 브로커 자격증명/세션 관리, 레이트리밋, 장애 시 reconciliation 운영 절차 신설.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `BrokerPort` 추상화와 단일 세션 어댑터를 제공하고(불변식 B), 코어만 주문/취소를 송신한다. 어댑터는 (a) 결정적 검증용 paper/stub 와 (b) **실제 KIS(한국투자증권) Open API 어댑터**를 모두 포함한다(OQ-1 결정). KIS 어댑터는 슬라이스에서 **KIS 모의투자 서버**로 검증하고, 실계좌 라이브 전환은 config 게이팅(운영 결정)으로 둔다.
- FR-2: 주문 집행 컴포넌트가 `pending_buy` 슬롯을 `FOR UPDATE SKIP LOCKED`(status=pending_buy AND broker_order_id IS NULL)로 claim 해 매수 주문을 송신하고 `broker_order_id` 를 기록한다(옵션 A 실행 큐, 멱등 가드로 중복 디스패치 방지).
- FR-3: 집행 루프가 `poll_fill` 로 체결을 **비동기 관측**(paper·KIS 동일 경로)해, 매수 체결 시 `pending_buy→holding`(avg_price/qty 반영), 거부/취소 시 `pending_buy→idle` 롤백을 코어가 단독 수행한다.
- FR-4: `pending_sell` 슬롯을 claim 해 매도 주문 송신, `poll_fill` 비동기 체결 관측으로 체결 시 `pending_sell→closed`(recovery 시 재진입 +1)로 진행한다.
- FR-5: 주문 핸드오프가 durable/멱등하다(옵션 A) — pending 슬롯 + `broker_order_id` 가 큐+멱등 가드이며, 재기동 시 `broker_order_id` 있는 pending 슬롯을 브로커 주문 상태와 대조하는 reconciliation 으로 미체결/중복을 정리한다(중복 주문 0). 별도 outbox 테이블은 두지 않는다.
- FR-6: baseline 문서(runtime-flows/interface-contract/project-structure/data-model)를 집행/브로커 레이어에 맞게 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 불변식 B — 브로커 세션/주문 송신은 코어 집행 경로에서만. 도메인/전략은 브로커 미접근.
- NFR-2: 도메인 순수성 유지 — `trex_core.domain.slot` 은 브로커/네트워크 의존 0(어댑터 격리, import 스윕).
- NFR-3: 멱등/내구성 — 동일 슬롯/주문에 대해 중복 주문이 발생하지 않으며, 크래시 재개 시 정확히 1회 의미를 보존한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 브로커 포트 + paper/stub 어댑터(불변식 B), 주문 집행 루프, 체결/거부 처리(슬롯 전이), durable 핸드오프 + reconciliation, baseline 갱신.
- In Scope (추가, OQ-1 결정)
- 실제 KIS Open API 어댑터(주문 송신/취소/체결 조회), KIS 모의투자 서버 검증, 세션/토큰 관리·레이트리밋.
- Out of Scope (후속 Discovery)
- safety floor(forced_close/EOD/손절 backstop, liveness gating, sell_evaluation_log) — 별도 Discovery(브로커 집행 위에 얹음).
- KIS **실계좌 라이브 go-live**(실자금 거래) — 어댑터는 구현하되 라이브 전환은 config 게이팅된 운영 결정으로 분리.
- 매수·매도 전략 데몬, API 인증/인가, 수평 확장.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 실주문 중복/유실 — 크래시 타이밍에 따라 중복 주문 또는 미체결 방치 위험.
- 완화 방안(OQ-3 결정=옵션 A): `broker_order_id` 멱등 가드(채워지면 재디스패치 안 함) + 재기동 reconciliation(broker_order_id 있는 pending 슬롯을 브로커 상태와 대조, FR-5). paper async 더블로 크래시 시나리오 테스트.
- R-2 (High): KIS 비동기 체결 — 주문 송신과 체결이 분리(체결 조회/폴링)되어 부분체결·지연·실패 처리 복잡.
- 완화 방안(OQ-2 결정): paper·KIS 통일 비동기 체결 계약(submit→poll). paper async 더블이 지연/부분/거부 체결을 **결정적으로 재현**해 운영과 동일 경로를 검증 + KIS 모의투자로 실 API 체결 확인 + 멱등 전이.
- R-5 (High): KIS 실 API — 자격증명/토큰 만료·레이트리밋·API 변경·실자금 손실 위험.
- 완화 방안: 슬라이스는 KIS **모의투자 서버**로만 검증, 실계좌 라이브는 config 게이팅 분리(Out of Scope). 자격증명은 .env/시크릿(REQ-010 패턴), 토큰 갱신·레이트리밋 어댑터 내 처리.
- R-3 (Medium): 도메인 순수성/단일 writer 침식 — 집행 컴포넌트가 broker+slot 을 함께 다뤄 경계가 흐려질 위험.
- 완화 방안: broker 는 adapters, 슬롯 쓰기는 SlotService 단일 경로(NFR-1/2), import 스윕 가드. 집행 루프(OQ-4)는 코어 내 단일 주체로 단일 writer/세션 유지(같은 프로세스 백그라운드).
- R-4 (Medium): 실거래 안전 — 잘못된 집행이 금전 손실.
- 완화 방안: 슬라이스 1 비실거래(paper), 안전 기본값, safety floor 는 후속이지만 본 설계가 그 hook 을 남긴다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 코어는 단일 인스턴스로 `daily_slot` 단일 writer + 단일 브로커 세션을 소유한다(수평 확장 비대상).
- A-2: 집행·체결의 결정적 검증은 paper/stub 로, 실 API 검증은 KIS **모의투자 서버**로 수행한다. 실계좌 라이브(실자금)는 본 슬라이스 비대상(config 게이팅 운영 결정).
- A-3: safety floor 는 본 Discovery 가 아니라 후속에서 다루되, 본 집행 설계가 그 hook(강제청산 소비 지점)을 남긴다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 브로커 어댑터 범위 — paper/stub 만(슬라이스 1) vs 실제 증권사(예: KIS) 어댑터까지.
- 상태: Answered (2026-06-04)
- 결정: **실제 KIS(한국투자증권) Open API 어댑터까지 구현.** paper/stub(결정적 테스트)와 KIS 어댑터를 모두 제공하고, KIS 는 **모의투자 서버**로 슬라이스 검증, 실계좌 라이브 go-live 는 config 게이팅으로 분리(Out of Scope). 근거: 실제 거래 능력 확보가 목표이고 KIS 모의투자로 안전하게 실 API 를 검증 가능.
- 반영 위치: `# 4`(코드 영향), `# 5` FR-1·In Scope·Out of Scope, `# 6` R-2/R-4·A-2. OQ-2(체결 모델)는 KIS 비동기 체결을 반드시 포함하도록 파급.
- OQ-2: 체결 처리 모델 — paper 도 KIS 와 동일한 **비동기 체결 구조**로 통일할지(검증 정확도).
- 상태: Answered (2026-06-04)
- 결정: **단일 비동기 체결 계약으로 통일.** `BrokerPort` 는 `submit_order`(→ accepted + broker_order_id) 와 `poll_fill`(체결 관측)로 분리하고, 집행 루프가 폴링으로 체결을 반영한다. paper/stub 은 즉시체결이 가능하더라도 **동기로 단축하지 않고** 동일한 submit→poll 구조를 따르되, 체결 시점·부분체결·거부를 **프로그래밍 가능한 결정적 더블**로 제공한다. KIS 는 체결 조회 API 폴링. 근거: 테스트(paper)와 운영(KIS)이 **같은 집행 코드 경로**를 타야 검증이 정확하고, paper 의 결정적 제어가 즉시체결보다 엣지 케이스(지연/부분/거부) 검증력이 높다.
- 반영 위치: `# 5` FR-1(paper 도 async 더블)·FR-3·FR-4, `# 6` R-2 완화, `# 7` S-1/S-2/S-6. 집행 루프 폴링 → OQ-4(별도 execution 루프) 방향 강화.
- OQ-3: 주문 핸드오프 durability 형태 — pending 슬롯 자체를 실행 큐로(orbit `claim ... FOR UPDATE SKIP LOCKED`) vs 별도 order outbox 테이블 vs intent_log 의 outbox 승격.
- 상태: Answered (2026-06-04)
- 결정: **옵션 A — pending 슬롯 자체가 실행 큐.** 별도 테이블 없이 `daily_slot.status IN (pending_buy,pending_sell) AND broker_order_id IS NULL` 을 `FOR UPDATE SKIP LOCKED` 로 claim, 주문 송신 후 `broker_order_id` 기록(멱등 가드 — 채워지면 재디스패치 안 함). reconciliation 은 `broker_order_id` 있는 pending 슬롯을 브로커 상태와 대조. dcy-002 `intent_log` 는 dedup 용도로 유지(outbox 승격 안 함, 옵션 C 지양). 부분체결/재시도가 본격화되면 별도 order 테이블(옵션 B)로 승격하는 hook 을 남긴다. 근거: 단순·orbit 검증·슬롯 단일 진실, 한 슬롯=한 주문 모델에 충분.
- 반영 위치: `# 4`(변경 항목), `# 5` FR-2·FR-5, `# 6` R-1 완화. 별도 outbox 테이블 미신설.
- OQ-4: 집행 트리거 — 별도 execution 워커 루프(주기적 claim) vs intent 수락 직후 인라인 디스패치.
- 상태: Answered (2026-06-04)
- 결정: **별도 execution 루프(데몬 프로세스 내 백그라운드).** 루프가 ① claim→주문 송신→broker_order_id 기록 ② poll_fill→체결 전이 ③ reconcile 를 주기적으로 수행한다. intent API 는 슬롯 pending 전이까지만(빠른 응답), 집행은 루프가 비동기. 인라인 nudge 는 선택적 후속 최적화로 보류. 근거: OQ-2(폴링 체결)·OQ-3(claim 큐+reconciliation)가 지속 집행 주체를 요구 → 루프가 정합. 단일 writer/세션은 같은 프로세스 내 단일 루프로 유지.
- 반영 위치: `# 4`(변경 항목), `# 5` FR-2~FR-5(집행 루프 주체), `# 6` R-3. 폴링 주기·배치 크기는 config(구현 세부).
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: paper 브로커로, 매수 intent → pending_buy → 집행 주문 송신(accepted) → **폴링 체결** → `holding` 까지 코어가 자동 진행한다.
- S-2: 매도 intent → pending_sell → 집행 → 폴링 체결 → `closed` + 재진입 +1 까지 자동 진행한다.
- S-3: 동일 슬롯에 대해 집행이 두 번 돌아도 중복 주문이 생기지 않는다(broker_order_id 멱등).
- S-4: 주문 송신 직후 크래시 시뮬레이션 후 재기동 → reconciliation 으로 중복 주문 없이 일관 상태로 수렴.
- S-5: 도메인 import 스윕에서 브로커/네트워크 의존이 0 으로 유지된다(불변식 B/순수성).
- S-6: paper async 더블이 비동기 체결(즉시/지연/부분/거부)을 결정적으로 재현하고, 동일 집행 경로가 KIS 모의투자에서도 동작한다(검증 정확도).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1/S-2 측정 방식: paper 브로커 통합테스트(전체 lifecycle) / 데이터 출처: 테스트 결과, 슬롯 상태·transition_log.
- S-3 측정 방식: 집행 중복 호출 테스트(같은 슬롯) / 데이터 출처: 브로커 주문 호출 수, broker_order_id.
- S-4 측정 방식: 크래시 주입 후 reconciliation 테스트 / 데이터 출처: 주문 수·슬롯 상태 일관성.
- S-5 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain` import 목록.
- S-6 측정 방식: paper 더블의 fill 시나리오(즉시/지연/부분/거부) 파라미터화 테스트 + KIS 모의투자 lifecycle / 데이터 출처: 테스트 결과, 체결 로그.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — 특히 safety floor·실 브로커 제외
- 기준: 운영자가 슬라이스 경계를 확인했다. → confirm-discovery 에서 문서대로 확정(2026-06-04).
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 측정 방식·출처 명시(본 문서 # 7).
- [x] High 영향 리스크(R-1 중복주문, R-2 KIS 체결, R-5 KIS 실 API) 처리 방침 확정
- 기준: paper/stub + KIS 모의투자 검증 + reconciliation + 라이브 게이팅으로 수용. → 운영자 수용(2026-06-04).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 0개 또는 잔여 전부 `Deferred` (OQ-1~OQ-4 전부 Answered → `Open` 0개)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다. → Confirmed By: Justin.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-003_20260604_broker-execution-and-outbox.md
- 참조 문서:
- docs/discovery/dcy-002_20260603_slot-core-daemon-http-api.md (슬롯 코어 데몬·intent·intent_log — 본 Discovery 가 집행을 잇는다)
- docs/project-structure.md, docs/runtime-flows.md, docs/interface-contract.md, docs/data-model.md (baseline — 본 Discovery 가 갱신 요구)
- trex-orbit `orbit-v2_plan.md` (불변식 B·Order Execution·forced_close 소비자 설계 참고)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~브로커 어댑터 범위~~ → 결정됨: paper/stub + **실제 KIS 어댑터**(모의투자 검증, 실계좌 go-live 는 게이팅) (OQ-1, 2026-06-04)
- ~~주문 핸드오프 durability~~ → 결정됨: **옵션 A(pending 슬롯=실행큐 + broker_order_id 멱등)**, 별도 테이블 미신설, B 승격 hook (OQ-3, 2026-06-04)
## CONFIRM
- ~~체결 모델~~ → 결정됨: paper·KIS 통일 **비동기 체결**(submit→poll), paper 는 결정적 async 더블 (OQ-2, 2026-06-04)
- ~~집행 트리거~~ → 결정됨: **별도 execution 루프**(데몬 내 백그라운드), 인라인 nudge 보류 (OQ-4, 2026-06-04)
- ~~범위 경계(deferral) + High 리스크 수용~~ → 확정됨: safety floor·KIS 실계좌 라이브·전략 데몬·인증을 후속으로 미루고 슬라이스(브로커 포트+paper/stub+KIS 모의투자+집행 루프+비동기 체결+reconciliation) 승인. R-1/R-2/R-5 완화책(멱등+reconciliation, paper async 더블+KIS 모의투자, 실계좌 게이팅) 수용 (confirm-discovery, 2026-06-04)
## DATA
- 없음 (별도 외부 데이터 요청 없음)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~OQ-4 전부 Answered(KIS 어댑터까지 / 통일 비동기 체결 / 옵션 A 실행큐 / 별도 execution 루프). 범위 경계(safety floor·KIS 실계좌·전략 데몬·인증 후속 Deferred)와 High 리스크(R-1/R-2/R-5) 완화책을 confirm-discovery 에서 운영자 확정·수용, CONFIRMED_BY 지정 완료. REQ Drafting 으로 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-04
@@ -1,214 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-004_20260604_portfolio-hub-ledger-integration
- 이슈명: portfolio-hub ledger 연동 — 체결을 외부 원장에 기록
- 작성 시각(KST): 2026-06-04
- 마지막 갱신 시각(KST): 2026-06-04 22:46
- 대체됨: 없음
- 후속 Discovery 참조: docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md (read 방향 후속)
- 생성된 REQ 참조: REQ-017, REQ-018, REQ-019, REQ-020
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: trex-orbit `app/clients/portfolio_client.py`(`post_ledger_trade` 등), orbit BAT-006/REQ-025(체결→portfolio-hub ledger write-back)
- 해석:
- BAT-004 로 체결까지 코어가 수행하지만, 체결을 **외부 회계/포지션 원장(portfolio-hub)에 기록하지 않는다**. 슬롯이 `portfolio_id` 를 운반하는데 소비처가 없다.
- 이번 Discovery 는 체결 시 portfolio-hub ledger 에 거래를 기록하는 통합을 더해 운영 회계 정확성을 확보한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-001~003 어디에도 portfolio-hub ledger 연동이 In Scope 가 아니었다(dcy-003 미명시 deferral).
- 기존 REQ 본문 수정이 아니라 신규 외부 통합(원장 기록)이라 `change-req` 가 아니라 새 Discovery 다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 코어 집행 루프(BAT-004)는 체결 시 슬롯을 `holding`/`closed` 로만 전이한다. portfolio-hub 등 **외부 원장에 거래를 기록하지 않는다**(상태탐색: portfolio/ledger 모듈 0).
- 슬롯 계약에 `portfolio_id` 가 있으나(dcy-001 부록 A, intent API 수신) **소비처가 없다**(배선만 됨).
- orbit 에는 `PortfolioHubClient.post_ledger_trade`(`POST /v1/assets/{asset_id}/ledger/trades`, `execution_id` 멱등, 409 `LedgerConflictError`)가 있어 체결을 원장에 기록한다.
- 기대 상태
- 체결(매수/매도) 시 코어가 portfolio-hub ledger 에 거래(asset/side/qty/price/fee/tax/executed_at)를 **멱등하게 기록**한다.
- portfolio-hub 일시 장애에도 원장 기록이 유실되지 않게 처리한다(재시도/보류).
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- portfolio-hub(외부 회계/포지션 서비스) 및 그 데이터 소비자
- 제약사항
- 도메인 순수성 — `trex_core.domain.slot` 은 portfolio-hub/네트워크 의존 0(어댑터 격리).
- 멱등 — 동일 체결(execution_id)이 원장에 중복 기록되지 않는다.
- 단일 기록 주체 — 코어 집행 경로만 원장에 기록(전략/도메인 미접근).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 매수 체결(`pending_buy→holding`) 시 코어가 portfolio-hub ledger 에 buy 거래를 기록 → 외부 포지션/잔고 반영.
- 매도 체결(`pending_sell→closed`) 시 sell 거래 기록 → 실현 손익/잔고 반영.
- portfolio-hub 가 잠시 불가 → 코어가 재시도/보류로 원장 기록을 결국 반영(유실 없음), 슬롯 전이는 별도 보장.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- LedgerPort + portfolio-hub 어댑터: `LedgerPort.record_trade(...)` + `PortfolioHubLedger`(orbit `post_ledger_trade` 패턴) + 결정적 테스트용 stub/in-memory 원장.
- 체결→원장 기록: 집행 루프의 fill 전이 시 거래를 원장에 기록(buy/sell), `execution_id` 멱등.
- 내구성: portfolio-hub 장애 시 재시도/보류(원장 기록 유실 방지) — 방식은 OQ.
- baseline 갱신: runtime-flows(원장 기록 흐름)·interface-contract(LedgerPort)·project-structure(ledger 어댑터)·data-model(원장 기록/필드).
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규: `trex_core/` 하위 LedgerPort + portfolio-hub 어댑터 + stub, 집행 루프 fill 경로에 ledger 기록 hook. config(portfolio-hub URL/토큰).
- 기존: ExecutionService fill 전이에 원장 기록 단계 추가.
- 문서 영향:
- runtime-flows/interface-contract/project-structure/data-model 갱신.
- 운영 영향:
- 신규 외부 의존(portfolio-hub) + 자격증명(.env). 장애 시 재시도/보류 운영 절차.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `LedgerPort.record_trade(asset, order_id, execution_id, side, qty, price, fee, tax, executed_at)` 추상화 + portfolio-hub 어댑터(`POST /v1/assets/{asset_id}/ledger/trades`) + 결정적 검증용 stub/in-memory 원장을 제공한다.
- FR-2: 집행 루프가 매수 체결(`pending_buy→holding`)·매도 체결(`pending_sell→closed`) 시 해당 거래를 원장에 기록한다. ledger path 의 `asset_id` 는 슬롯 `portfolio_id`(=asset_id, OQ-1 결정)를 사용하고 side/qty/price/체결데이터를 함께 보낸다.
- FR-3: 원장 기록은 correlation_id(`{asset_id}:{order_id}:{execution_id}`)로 멱등하다 — 동일 체결 재기록 시 로컬 dedup + portfolio-hub 중복/409(LedgerConflict)를 안전 처리하고 중복 원장 0. execution_id 는 결정적 합성(OQ-3).
- FR-4: 체결 시 원장 기록을 ledger outbox(work item)에 적재하고 재시도 루프가 portfolio-hub write 를 수행한다(orbit LedgerWriter 패턴). outcome = completed/duplicate_completed/retryable_failure/conflict_blocked. 슬롯 전이와 분리되어 일시 장애에도 유실 없이 결국 기록(conflict 는 운영자 해소).
- FR-5: baseline 문서(runtime-flows/interface-contract/project-structure/data-model)를 ledger 레이어에 맞게 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 도메인 순수성 유지 — `trex_core.domain.slot` 은 portfolio-hub/네트워크 의존 0(어댑터 격리, import 스윕).
- NFR-2: 멱등/무유실 — 동일 execution_id 중복 기록 없음 + 장애 시 결국 기록(at-least-once + dedup).
- NFR-3: 단일 기록 주체 — 코어 집행 경로만 원장에 기록.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- LedgerPort + portfolio-hub 어댑터(post_ledger_trade) + stub, 체결→원장 기록(buy/sell), execution_id 멱등, 장애 재시도/보류, baseline 갱신.
- Out of Scope (후속 Discovery)
- `post_portfolio_transaction`(입출금 등 거래 외 트랜잭션), asset master 동기화·get_assets/get_portfolios 연동.
- portfolio-hub ↔ 코어 양방향 정합(reconciliation), 포지션 재구성.
- safety floor, 매수·매도 전략 데몬, 실계좌 go-live(타 Discovery).
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 원장 기록 유실/중복 — 체결은 됐는데 원장 미기록(장애) 또는 중복 기록 시 회계 부정확.
- 완화 방안(OQ-2/OQ-3): correlation_id 멱등(409/duplicate 안전) + ledger outbox 재시도 루프(무유실) + conflict_blocked 운영자 해소. stub 으로 결정적 테스트.
- R-2 (해소, OQ-1): asset_id 해석 — orbit 근거로 `asset_id == portfolio_id` 확정. 슬롯 `portfolio_id` 를 path 로 직접 사용(추가 매핑/조회 없음).
- R-3 (Medium): 외부 의존 결합 — portfolio-hub 장애가 집행 경로를 막을 위험.
- 완화 방안: 원장 기록을 슬롯 전이와 분리(전이는 우선, 기록은 재시도/보류). 어댑터 격리(NFR-1).
- R-4 (해소, OQ-3): fee/tax/execution_id 출처 — execution_id 결정적 합성(KIS 규칙/paper synth), fee 는 BrokerPort 계산(orbit calculate_fee), tax 기본 0(정밀 모델 후속).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: portfolio-hub 는 orbit 과 동일 계약(`POST /v1/assets/{asset_id}/ledger/trades`, execution_id 멱등, 409 conflict)을 제공한다.
- A-2: 코어는 단일 인스턴스로 원장 기록의 단일 주체다.
- A-3: 자격증명(URL/토큰)은 .env(REQ-010 패턴)로 주입한다(orbit `TREX_PORTFOLIO_HUB_*`).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: asset_id 해석 — portfolio-hub `{asset_id}` 를 슬롯의 무엇으로 채울지(`portfolio_id` == asset_id? symbol→asset_id 매핑? portfolio-hub get_assets 조회?).
- 상태: Answered (2026-06-04)
- 결정: **`asset_id == portfolio_id`** — 슬롯의 `portfolio_id` 를 ledger path param 으로 직접 사용. symbol 매핑/get_assets 조회 불필요. 근거: trex-orbit `app/domain/ledger.py`("asset_id == portfolio_id for this system"), migration 003(Order Execution 이 `{portfolio_id}` path 로 write-back, 매수 커밋 시 슬롯에 캐시), `coordinator.py`(`asset_id = asset_id or portfolio_id`). trex-core 는 이미 buy intent 가 `portfolio_id` 를 공급해 슬롯에 캐시하므로 추가 조회 없이 그대로 사용.
- 반영 위치: `# 5` FR-1·FR-2, `# 6` R-2(해소). buy intent 의 `portfolio_id` 필수 여부는 구현 시 결정(없으면 기록 skip 또는 거부).
- OQ-2: 기록 내구성 방식 — fill 전이와 동일 경로 동기 기록 vs 별도 ledger outbox 비동기.
- 상태: Answered (2026-06-04)
- 결정: **별도 ledger outbox(work item) + 재시도 루프** (orbit `LedgerWriter`/recovery_work_item 패턴 차용). 체결 시 ledger 기록을 outbox 에 적재(슬롯 전이와 분리 → 전이는 우선, 기록은 eventually-consistent), 루프가 portfolio-hub write 시도. correlation_id = `{asset_id}:{order_id}:{execution_id}` 로 dedup, outcome 어휘 = completed / duplicate_completed / retryable_failure(재시도) / conflict_blocked(409, 운영자 해소). 근거: orbit `app/domain/ledger.py` 가 동일 패턴으로 무유실+멱등을 보장.
- 반영 위치: `# 4`, `# 5` FR-3·FR-4, `# 6` R-1. (BAT-004 의 옵션 B outbox 와 유사 구조 — ledger 전용.)
- OQ-3: 체결 데이터(execution_id/fee/tax) 출처 — KIS 체결조회/주문응답에서 추출 가능 범위, paper 더블의 합성 값.
- 상태: Answered (2026-06-04)
- 결정: **execution_id 는 결정적 합성** — KIS: `kis-daily-ccld:{ord_dt}:{ord_gno_brno}:{odno}:{tot_ccld_qty}`(orbit kis.py 규칙 차용), paper: 결정적 synth(예: `paper:{broker_order_id}:{filled_qty}`). **fee 는 BrokerPort fee 계산**(orbit `calculate_fee(side, amount)` 차용), **tax 는 기본 0**(정밀 세금 모델은 후속). FillStatus/브로커가 execution_id·fee 를 운반하도록 contract 확장.
- 반영 위치: `# 5` FR-1·FR-3, `# 6` R-4. (BrokerPort 에 fee 계산 + FillStatus.execution_id 추가는 구현 세부.)
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: stub 원장으로, 매수 체결 시 buy 거래가 원장에 1건 기록되고 슬롯이 holding 으로 전이된다.
- S-2: 매도 체결 시 sell 거래가 기록되고 슬롯이 closed 로 전이된다.
- S-3: 동일 execution_id 재기록 시 원장에 중복이 생기지 않는다(멱등).
- S-4: 원장 기록이 일시 실패해도 재시도/보류로 결국 기록되고, 슬롯 전이 일관성이 유지된다.
- S-5: 도메인 import 스윕에서 portfolio-hub/네트워크 의존이 0 으로 유지된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1/S-2 측정 방식: stub 원장 통합테스트(체결 lifecycle) / 데이터 출처: 테스트 결과, 원장 기록 목록·슬롯 상태.
- S-3 측정 방식: 동일 execution_id 재기록 테스트 / 데이터 출처: 원장 엔트리 수.
- S-4 측정 방식: 원장 실패 주입 후 재시도 테스트 / 데이터 출처: 기록 결과·슬롯 상태.
- S-5 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain` import 목록.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — 특히 거래 외 트랜잭션/asset 동기화 제외
- 기준: 운영자가 슬라이스 경계를 확인했다. (운영자 확정 2026-06-04 — 문서대로 수용)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 측정 방식·출처 명시(본 문서 # 7).
- [x] High 영향 리스크(R-1 원장 유실/중복) 처리 방침 확정
- 기준: execution_id 멱등 + 재시도/보류로 수용 가능한지 운영자 확인. (운영자 확정 2026-06-04 — 멱등+outbox 재시도 수용)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 0개 또는 잔여 전부 `Deferred` (OQ-1~OQ-3 전부 Answered → `Open` 0개)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다. (Confirmed By = Justin Ko)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md
- 참조 문서:
- docs/discovery/dcy-003_20260604_broker-execution-and-outbox.md (집행/체결 — 본 Discovery 가 체결에 원장 기록을 더한다)
- docs/project-structure.md, docs/runtime-flows.md, docs/interface-contract.md, docs/data-model.md (baseline — 본 Discovery 가 갱신 요구)
- trex-orbit `app/clients/portfolio_client.py` (post_ledger_trade 계약 참고)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
- ~~asset_id 소스~~ → 결정됨: `asset_id == portfolio_id`(슬롯 portfolio_id 직접 사용, orbit 근거) (OQ-1, 2026-06-04)
- ~~기록 내구성~~ → 결정됨: 별도 ledger outbox + 재시도 루프(orbit LedgerWriter 패턴), correlation_id dedup, outcome 어휘 (OQ-2, 2026-06-04)
## CONFIRM
- ~~체결 데이터 출처~~ → 결정됨: execution_id 결정적 합성(KIS 규칙/paper) + fee BrokerPort 계산 + tax 0 (OQ-3, 2026-06-04)
- ~~범위 경계 확정~~ → 결정됨: 거래 외 트랜잭션(`post_portfolio_transaction`)·asset 동기화·reconciliation 을 후속 Discovery 로 미루는 In/Out Scope 경계를 문서대로 확정 (운영자 Justin Ko, 2026-06-04)
## DATA
- ~~portfolio-hub ledger API 계약/자격증명~~ → REQ/delivery 단계 계측·확정으로 위임(비차단): 설계는 orbit `post_ledger_trade` 검증 패턴 + stub/in-memory 결정론 테스트(S-1~S-5)로 REQ 초안 작성에 충분하며, 실제 계약/자격증명(orbit 동일 가정 A-1) 검증은 REQ acceptance/verification evidence 로 수집. (운영자 Justin Ko, 2026-06-04)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~OQ-3(asset_id 소스·기록 내구성·체결 데이터 출처) 전부 Answered, 범위 경계·R-1 처리 방침 운영자 확정(문서대로). portfolio-hub ledger API 계약/자격증명(DATA)은 orbit 동일 가정 + stub 결정론 검증으로 REQ 초안 작성을 막지 않아 REQ/delivery 계측 evidence 로 위임(비차단). 저장소 사실 관계는 현재 상태와 일치(ledger 모듈 0, portfolio_id 배선만 됨, BAT-004 fill 전이 확인). REQ Drafting 으로 인계한다.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-04 21:47
@@ -1,333 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-005_20260604_portfolio-hub-asset-snapshot-api
- 이슈명: portfolio-hub 종목 스냅샷 read API — 전략 모듈에 전략 실행 대상 자산 리스트 제공
- 작성 시각(KST): 2026-06-04 22:46
- 마지막 갱신 시각(KST): 2026-06-06 00:20
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-027, REQ-028, REQ-029, REQ-030, REQ-031, REQ-032, REQ-033
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: dcy-004(portfolio-hub ledger write 통합, BAT-006 진행 중)의 Out of Scope `asset master 동기화·get_assets/get_portfolios 연동`. orbit `app/clients/portfolio_client.py`(read 계약 get_assets/get_portfolios 패턴). **dcy-006(confirmed) — trex-core ↔ 전략 모듈 통신 transport 를 RabbitMQ 로 확정(HTTP 표면 0). 본 Discovery 의 스냅샷 read 는 그 transport(RabbitMQ request-reply RPC) 위에서 전달된다.**
- 해석:
- dcy-004 는 체결을 portfolio-hub 원장에 기록하는 **write 방향**이었고, 본 Discovery 는 그 반대인 **read 방향** — portfolio-hub 종목 스냅샷을 trex-core 가 전략 모듈에 제공한다. dcy-004 가 명시적으로 후속으로 미룬 영역을 이어받는다.
- **transport/계약 분리**: 본 Discovery 는 스냅샷의 **logical 계약**(응답 단위·필드·마스킹·view 모드·strategy_key 필터·신선도)을 정의하며 **transport-무관**이다. 전달 방식(HTTP 아님 — dcy-006 의 RabbitMQ RPC)은 dcy-006 소관이고, 본 문서는 그 transport 위에 logical 계약을 얹는다.
- active-cycle 판정(BAT-006 draft 진행 중)이지만 명시적 신규 입력이고, BAT-006(write)과 read 방향이 분리되어 범위 충돌이 없으므로 병렬 독립 주기로 진행한다. core 경유의 핵심 가치는 단순 패스스루가 아니라 **core 의 권위 슬롯 상태를 겹쳐(slot overlay) 전략 실행 대상 자산을 선별(pending·warning·pause 마스킹)** 해 제공하는 것이다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-004 는 write 방향(원장 기록)만 In Scope 였고 read 방향(get_assets/get_portfolios)은 명시적으로 Out of Scope(후속)였다. 본 Discovery 는 그 후속 read 통합이다.
- 기존 REQ 본문 수정이 아니라 신규 외부 read 통합 + 신규 read 계약(스냅샷 응답)이라 `change-req` 가 아니라 새 Discovery 다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- trex-core 의 read 경로는 슬롯/capacity 조회와 intent 조회뿐이고 portfolio-hub 종목을 노출하는 read 경로가 없다. (dcy-006 구현으로 통신 transport 가 RabbitMQ 로 전환됨 — `adapters/api` HTTP 어댑터 제거, `adapters/messaging`(rabbitmq/topology) 추가, daemon 이 `trex.snapshot.req`/`trex.intent.req` RPC consumer 운영. 따라서 본 스냅샷 read 는 그 snapshot RPC 위에 portfolio-hub 종목 logical 계약을 얹는다 — portfolio-hub read 포트/어댑터는 아직 없다.)
- portfolio-hub read 어댑터(get_assets/get_portfolios)가 trex-core 에 없다. dcy-004/BAT-006 은 write 방향 `LedgerPort` + portfolio-hub 어댑터만 추가하며, read 포트는 다루지 않는다.
- 결과적으로 전략 모듈이 "무엇을 매매할지" 정할 portfolio-hub 종목 정보를 trex-core 를 통해 받을 경로가 없다.
- 기대 상태
- trex-core daemon 이 설정 가능한 시각(기본 07:30 KST, .env)에 일일 스케줄로 portfolio-hub 에서 자신의 `daemon_id` universe(= `managed_by_daemon_id` 가 해당 daemon 인 portfolio 집합)를 가져와 스냅샷으로 보유한다. universe 항목은 portfolio 단위로 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON)를 담고 **가격(price)은 제외**한다.
- 전략 모듈 요청 시 trex-core 가 이 스냅샷을 기반으로 **portfolio-hub 보유 자산(holding 슬롯 자산 포함)을 담되 pending·warning·pause 상태 자산은 마스킹한 전략 실행 대상 자산 리스트**를 반환한다(OQ-4).
- 응답은 요청 `view` 파라미터(`all` 기본 | `buyable` | `held`)로 base 집합을 고르고 envelope(`assets[]` + `available_slots`·`degraded` 등 meta)로 반환한다(OQ-11/OQ-12/OQ-6). 가용 슬롯이 0 이어도 보유 자산은 계속 반환되며 신규 매수 가용성은 `available_slots` meta 로 신호한다.
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- 전략 모듈(매수/매도/position, trex-core 외부 소비자) 및 portfolio-hub(외부 자산/포지션 서비스)
- 제약사항
- 도메인 순수성 — `trex_core.domain` 은 portfolio-hub/네트워크 의존 0(read 어댑터 격리).
- 단일 통합 경계 — portfolio-hub 자격증명/계약 접점은 trex-core 어댑터(dcy-004 재사용)로 한정하며, 매수 전략 등 모든 소비자는 core 스냅샷 계약을 경유한다(portfolio-hub 직접 호출 금지, OQ-3).
- core 는 슬롯/capacity 의 권위 단일 writer(불변식 A) — slot overlay 는 이 권위 상태를 사용한다.
- transport 종속성 — 스냅샷 read 의 전달은 dcy-006(confirmed)의 RabbitMQ request-reply RPC 를 사용한다(HTTP 아님). 본 Discovery 는 logical 계약만 정의하고 transport 세부(토폴로지·delivery)는 dcy-006 소관이다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략 모듈이 자기 portfolio(전략 실행 대상 자산)를 trex-core 스냅샷 API 로 요청 → portfolio-hub 보유 자산(holding 슬롯 자산 포함)을 받되, pending·warning·pause 상태 자산은 마스킹되어 빠진 리스트를 받는다.
- 모든 슬롯이 차 있어 더 살 수 없는 상황 → 보유 자산은 계속 응답에 포함되고(`view=all`/`held`), 신규 매수 가능 여부는 `available_slots` meta 로 신호한다. `view=buyable` 은 `available_slots=0` 이면 `[]` 를 반환한다(OQ-11/OQ-12).
- 막 매수 주문되어 슬롯이 pending(미체결)인 종목 → 마스킹되어 전략 모듈 응답에서 빠져 중복 매수를 막고, 체결되어 holding 이 되면 보유 자산으로 응답에 포함된다.
- 스냅샷 페치 시 portfolio-hub 포지션과 실제 브로커 계좌 포지션이 어긋난 자산 → `warning` 으로 표시되어 그 자산은 응답 리스트에서 마스킹되고 매매 전략이 수행되지 않는다.
- 전략 모듈이 자기 `strategy_key` 를 파라미터로 넣어 포트폴리오를 요청 → 그 `strategy_key` 가 등록된 자산만 받는다. 파라미터를 생략하면 전체 자산을 받는다(여러 전략 모듈이 하나의 스냅샷 API 를 공유하되 자기 책임 자산만 조회).
- 운영자가 특정 자산을 런타임에 pause → 전략 모듈이 포트폴리오를 요청해도 그 자산은 응답에서 마스킹되어 매매가 멈추고, resume 하면 다시 응답에 포함된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- portfolio-hub read 포트 + 어댑터: `PortfolioHubReadPort.get_portfolios(daemon_id)` 추상화 + portfolio-hub 어댑터(`managed_by_daemon_id` 로 daemon universe 조회, dcy-004 어댑터/자격증명 재사용) + 결정론 stub. 전략파라미터는 trex-core 가 해석하지 않는 opaque JSON 으로 운반한다.
- 일일 스냅샷 갱신: 설정 시각(기본 07:30 KST, .env)에 스케줄러(신규 daemon 컴포넌트)가 universe 를 페치해 스냅샷을 갱신한다. 현금/포지션은 baseline 에 더해, 체결 시 portfolio-hub ledger write 응답이 반환하는 변경 후 권위 cash/position 으로 갱신한다(옵션 D — 당초 옵션 C 의 core 로컬 체결 replay 보정을 대체).
- 포지션 정합 검증 + warning: 페치 시 portfolio-hub 포지션 vs 실제 브로커 계좌 포지션을 대조하고, 불일치 자산은 `warning` 상태로 응답 리스트에서 마스킹한다.
- 종목 스냅샷 read 계약: trex-core 가 보유 스냅샷을 read 계약(요청-응답, transport 는 dcy-006 RabbitMQ RPC)으로 제공하고, **holding 포함·pending·warning·pause 마스킹**한 전략 실행 대상 자산 리스트를 반환한다(slot overlay, OQ-4).
- strategy_key 필터: 스냅샷 read 요청이 선택적 `strategy_key` 파라미터(요청 payload, dcy-006 OQ-2)를 받아, 주어지면 해당 `strategy_key` 가 등록된(= `active_strategy_id` 일치) 자산만 반환하고, 없으면 (pending·warning·pause 마스킹 후) 모든 자산을 반환한다. `strategy_key` 는 사람이 읽는 안정 유일 slug(예: `target-value-v1`), 단일 `active_strategy_id` 정확 일치(OQ-9).
- 런타임 pause/resume: trex-core 가 보유한 스냅샷 universe 의 개별 항목을 사용자가 런타임에 일시 중단(pause)/재개(resume)할 수 있고, pause 된 항목은 전략 모듈의 포트폴리오 요청 결과에서 제외한다. pause 단위·제어 방식·영속성은 OQ-10.
- 응답 view 모드 + envelope: 응답 base 집합을 요청 `view`(`all` 기본|`buyable`|`held`)로 선택하고 envelope(`assets[]` + `view`·`available_slots`·`degraded`·`reason` meta)로 반환한다(OQ-11/OQ-12/OQ-6). capacity 0 이어도 보유 자산은 반환, `buyable` 만 `available_slots=0` 시 `[]`.
- baseline 갱신: interface-contract(read 포트/메시지 계약)·runtime-flows(스냅샷 조회 흐름, RabbitMQ RPC)·project-structure(read 어댑터) 갱신. (dcy-006 의 transport baseline 갱신과 정합.)
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규: `trex_core/application` read 포트, 신규 `adapters/portfolio` read 어댑터 + stub, 스냅샷 read 를 dcy-006 messaging transport(RabbitMQ RPC, `adapters/messaging`)로 노출 + 응답 스키마(transport-무관), slot overlay(점유 종목 제외) + 현금/포지션 write-응답 갱신 로직(`cash_balance_after`/`asset_quantity_after`, 옵션 D), 일일 스냅샷 스케줄러(런타임 신규), 포지션 정합 검증 + `warning` 상태, `config` 에 스냅샷 페치 시각 주입(`daemon_id` 는 portfolio-hub 토큰에서 도출 — 별도 config 불필요, OQ-7), `strategy_key` 필터(요청 payload → `active_strategy_id` 매칭), 스냅샷 항목 pause/resume 상태 저장(Postgres, portfolio_id 키, refresh 무관) + 제어 표면(CLI + dcy-006 RPC 제어 메시지) + 응답 산출 시 pause 마스킹(전역).
- (dcy-006 구현 완료: `adapters/api` 제거·`adapters/messaging` 추가·`trex.snapshot.req` RPC consumer 존재. 스냅샷 read 는 이 RPC consumer 위에 portfolio-hub 종목 logical 계약을 채워 구현한다.)
- 기존: capacity/slot 조회 재사용(점유 종목·가용 슬롯 판정), 브로커 계좌 포지션 조회(BrokerPort 잔고조회 = KIS 주식잔고조회 `TTTC8434R`/`VTTC8434R`, `pdno`+`hldg_qty`, symbol 집계 — 정합 검증용, OQ-8) 재사용/확장.
- 문서 영향:
- interface-contract/runtime-flows/project-structure 갱신.
- 운영 영향:
- portfolio-hub read 의존 추가(dcy-004 자격증명 재사용). 캐시 도입 시 신선도/무효화 운영 고려.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: trex-core 가 종목 스냅샷을 read 계약(요청-응답)으로 제공한다. transport 는 dcy-006 의 RabbitMQ request-reply RPC 이며(HTTP 아님), 전략 모듈이 메시지로 소비한다. 스냅샷 단위는 daemon 이 관리하는 portfolio universe 이며, 항목당 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON)를 담고 가격(price)은 제외한다. (OQ-1 Answered)
- FR-2: 스냅샷 응답(전략 실행 대상 portfolio 자산 리스트)은 **portfolio-hub 에 보유된 자산을 기본 포함하며, holding 슬롯의 자산도 포함**한다(전략 모듈이 보유 포지션까지 받아 관리). 단 다음 상태의 자산은 **마스킹**하여 응답에서 제외한다: (a) 슬롯 상태 pending(미체결 in-flight), (b) 자산 상태 warning(브로커 실제 position 불일치, FR-7), (c) 사용자 pause(FR-9). 포함/마스킹 판정은 core 권위 슬롯 상태 + 정합 검증 + pause 상태를 사용하며 portfolio-hub 보유 여부보다 우선한다(slot overlay). 본 조항은 기본(default) view(`all`)의 구성 규칙이며, 응답 베이스 집합은 요청 `view` 파라미터(`all`/`buyable`/`held`, OQ-11/OQ-12)로 선택될 수 있고 마스킹은 선택된 베이스 위에 균일 적용된다. (변경 2026-06-05, OQ-4 결정: 종전 'pending·holding 점유 종목 제외, 매수 후보 리스트' → 'holding 포함, pending·warning·pause 만 마스킹'.)
- FR-3: 응답은 **envelope** 로 제공한다(자산은 항상 `assets: []` 배열, null 금지; meta: `view`·`available_slots`·`degraded`·`reason`) (OQ-6). 자산 base 집합은 요청 `view` 파라미터로 선택한다(OQ-11/OQ-12) — **`all`(기본, universe 전체) | `buyable`(universe − 슬롯 점유) | `held`(holding 슬롯만)**. 마스킹(pending·warning·pause)은 전 모드 균일 적용(OQ-4; pending 은 모든 모드 마스킹이라 `held`=holding 만). capacity 는 `available_slots` meta 로 신호하며 `buyable` 은 `available_slots=0` 이면 `[]`. (종전 'capacity 0 → 매수 후보 빈 리스트'는 OQ-4 holding 포함 + 본 결정으로 대체.)
- FR-4: portfolio-hub read 접근을 `PortfolioHubReadPort.get_portfolios(daemon_id)` 추상화 + portfolio-hub 어댑터(`managed_by_daemon_id` 기준) + 결정론 stub 로 제공하고, dcy-004 의 portfolio-hub 어댑터/자격증명을 재사용한다. `daemon_id` 는 portfolio-hub 자격증명/토큰에서 도출하며(OQ-7, 단일 진실원) 별도 config 값이 아니다(필요 시 whoami 1회 도출·캐시). 전략파라미터는 opaque JSON 으로 통과시킨다(trex-core 가 스키마를 해석하지 않음).
- FR-5: baseline 문서(interface-contract/runtime-flows/project-structure)를 read 포트·스냅샷 read 계약(RabbitMQ RPC)·read 어댑터·일일 스케줄러에 맞게 갱신한다(dcy-006 transport baseline 과 정합).
- FR-6: trex-core daemon 이 설정 가능한 시각(기본 07:30 KST, .env 로 지정)에 일일 스케줄로 `daemon_id` universe 를 페치해 스냅샷을 보유한다. universe 멤버십·전략파라미터는 일일 스냅샷으로 고정하고, 현금/포지션은 스냅샷 baseline 에 더해 **체결 시 portfolio-hub ledger write 응답의 `cash_balance_after` / `asset_quantity_after` 권위 값으로 갱신한다(옵션 D)**. (포지션/현금의 ground truth 는 portfolio-hub 이며 응답값으로 덮어써 로컬 누적 보정 drift 를 제거. portfolio-hub 가 ledger write 를 회계 단일 권위로 단일 트랜잭션 처리하고 `*_after` 를 반환하기로 확정 — A-7, portfolio-hub#8. portfolio-hub 배포(REL-003 등) 시점까지 구현 대기.)
- FR-7: 스냅샷 페치 시 각 portfolio 의 portfolio-hub 포지션과 실제 브로커 계좌 포지션이 **수량 정확 일치**하는지 검증한다(symbol 단위 집계, 허용 오차 없음 — OQ-8). 불일치 자산은 `warning` 상태로 표시·응답 리스트에서 마스킹하고 해당 자산에 매매 전략을 수행하지 않으며, **다음 정합 성공 시 자동 해제**한다(영속 hold 는 pause/FR-9). 브로커 포지션은 KIS 주식잔고조회(`TTTC8434R`/`VTTC8434R`, `pdno`+`hldg_qty`)를 BrokerPort 로 조회한다.
- FR-8: 스냅샷 read 요청은 선택적 `strategy_key` 파라미터(요청 payload, dcy-006 OQ-2 매핑)를 받는다. `strategy_key` 가 주어지면 해당 `strategy_key` 가 등록된(= 자산의 `active_strategy_id` 와 일치) 자산만 응답 리스트에 포함하고, `strategy_key` 가 없으면 (pending·warning·pause 마스킹 후) 모든 자산을 반환한다. 여러 전략 모듈이 같은 스냅샷 API 를 공유하되 각자 자기 책임 자산만 받는다. `strategy_key` 는 사람이 읽을 수 있는 안정 유일 slug(예: `target-value-v1`)이며 자산의 단일 `active_strategy_id` 와 대소문자 구분 정확 일치로 매칭한다(OQ-9; 다중 등록은 범위 밖). `strategy_key` 필터는 `view` 파라미터와 **직교 합성**된다(OQ-12).
- FR-9: trex-core 가 보유한 스냅샷 universe 의 개별 항목을 사용자가 런타임에 일시 중단(pause)/재개(resume)할 수 있다. pause 된 항목은 전략 모듈의 포트폴리오 요청 결과(FR-2)에서 제외한다. 응답 리스트는 holding 포함을 기본으로 (pending 마스킹) ∧ (warning 마스킹) ∧ (pause 마스킹) ∧ (strategy_key 필터, 주어진 경우)의 합성으로 산출된다. pause 단위는 `portfolio_id`(=asset_id), 제어는 **CLI + dcy-006 RPC 제어 메시지**, **Postgres 영속**(재시작·일일 refresh 후 유지), **전역 적용**, resume 시 다음 요청부터 즉시 복귀(OQ-10). pause(수동·영속) vs warning(자동·일시)은 독립 overlay·OR 합성.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 도메인 순수성 유지 — read 포트는 application 계약, 어댑터는 adapters 레이어 격리. `trex_core/domain` 은 portfolio-hub/네트워크 의존 0(import 스윕).
- NFR-2: 일관성 — 점유 종목 제외/가용 슬롯 판정은 core 의 권위 슬롯 상태(강한 일관성)를 사용한다. 현금/포지션은 일일 baseline + 체결 시 portfolio-hub ledger write 응답의 `cash_balance_after` / `asset_quantity_after` 권위 값 갱신으로 유지하고(옵션 D), 스냅샷 페치 시 브로커 계좌와 정합 검증한다(불일치 자산 `warning` 제외).
- NFR-3: 가용성 degrade — portfolio-hub 가 불가해 권위 cash/position 유지가 불가하면 **명시적 degraded 신호(reason code + sanitize 된 message 포함) + 신규 전략 매매 보수적 중단**(OQ-5; 빈 리스트/silent stale 금지). core 자율 보호 매도는 유지(ledger 는 outbox 로 결국 기록). dcy-006 OQ-4 와 공통 '보수적 안전' 철학.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- portfolio-hub read 포트 + 어댑터 + stub, 종목 스냅샷 read API, holding 포함·pending·warning·pause 마스킹(slot overlay, OQ-4), capacity 0 응답 재정의(OQ-11), 설정 시각 일일 스냅샷 + 현금/포지션 write-응답 갱신(옵션 D), 포지션 정합 검증 + `warning` 제외, baseline 갱신.
- Out of Scope (후속 Discovery)
- 매수 전략 의사결정 로직 자체(점수화·랭킹·진입 타이밍) — 전략 모듈 책임.
- portfolio-hub ↔ core 양방향 reconciliation, 포지션 재구성, asset master 영속 동기화.
- 매수/매도 전략 데몬, safety floor, 실계좌 go-live.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Medium): stale 스냅샷 — 일일 캐시한 현금/포지션이 장중에 어긋날 수 있다.
- 완화 방안(OQ-2 Answered, 옵션 D): 현금/포지션을 baseline + 체결 시 portfolio-hub ledger write 응답의 권위 cash/position 으로 갱신해 유지. daemon 이 유일 매매 주체이고 장중 입출금이 없다는 전제(A-6)로 정확. 추가로 페치 시 브로커 계좌 정합 검증(FR-7). 잔여 갭: 체결→write flush 사이에는 baseline 값이라 현금이 잠시 stale 할 수 있으나, 중복 매수 배제는 slot overlay(FR-2)가 즉시 담당.
- R-2 (Medium): in-flight race — dcy-004 ledger outbox 가 eventually-consistent 라, 막 체결한 종목이 portfolio-hub 포지션에 아직 반영되기 전 중복 노출될 수 있다.
- 완화 방안(OQ-4 Answered): pending 슬롯 마스킹으로 미체결 in-flight 종목을 응답에서 제외해 중복 노출을 차단한다. 체결 완료(holding)된 자산은 보유 자산으로 포함되며, 전략 모듈이 보유 상태를 보고 재매수하지 않는다(holding 자산의 보유 수량 표기 정합은 core 권위 슬롯 상태 기준 — 설계 시 확정). (종전 'pending·holding 동시 제외'에서 'pending 만 마스킹, holding 포함'으로 변경.)
- R-3 (Medium): 가용성 결합 — portfolio-hub 장애가 전략 read 를 막는다.
- 완화 방안(OQ-5 Answered): 권위 cash/position 유지 불가 시 명시적 degraded 신호로 신규 매매 보수적 중단(stale 매매·누적 drift 차단). core 보호 매도는 유지(outbox 로 ledger 결국 기록). 어댑터 격리(NFR-1).
- R-4 (Low): 책임 경계 확장 — core 가 portfolio-hub read gateway 로 비대해질 위험.
- 완화 방안: 모든 소비자가 core 를 경유하되(OQ-3 Answered), core 의 부가가치(slot overlay·현금/포지션 보정·정합 검증)로 범위를 한정하고 단순 패스스루 프록시는 지양한다.
- R-5 (Medium): 포지션 drift — portfolio-hub 가 실제 브로커 계좌와 어긋나면(과거 누락·정합 오류) 스냅샷 baseline 자체가 틀린다. (옵션 D 로 전환해 core 로컬 replay 누적 drift 는 제거되었으나, portfolio-hub ↔ 브로커 계좌 간 drift 는 여전히 남는다.)
- 완화 방안(FR-7, OQ-8 Answered): 스냅샷 페치 시 portfolio-hub 포지션 vs 브로커 계좌 포지션 **수량 정확 일치**(symbol 집계) 검증, 불일치 자산은 `warning` 으로 매매 제외(보수적 안전), 다음 정합 성공 시 자동 해제. 브로커 출처 = KIS 주식잔고조회.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: portfolio-hub 는 daemon 인증(`auth_tokens.daemon_id`)으로 자신이 관리하는 portfolio 집합을 조회하는 read 계약을 제공한다. (근거: portfolio-hub `app/api/v1_portfolios.py`·`v1_user_info.py` 의 `auth.daemon_id` 스코프)
- A-2: trex-core 는 슬롯/capacity 의 권위 단일 소스이며, 점유 종목·가용 슬롯 판정에 자기 상태를 사용할 수 있다.
- A-3: 전략 모듈은 trex-core 외부 소비자이며 dcy-006 transport(RabbitMQ request-reply RPC)로 스냅샷을 받는다(HTTP 아님). logical 계약(필드/마스킹/view 모드/strategy_key)은 transport-무관.
- A-4: dcy-004 의 portfolio-hub 어댑터/자격증명(.env `TREX_PORTFOLIO_HUB_*`)을 read 방향에서도 재사용한다.
- A-5: portfolio-hub 의 `daemon` 은 1급 엔티티이며 portfolio 가 `managed_by_daemon_id` 로 daemon 에 귀속된다(1 daemon : N portfolio, 각 portfolio ≈ 1 asset, asset_id == portfolio_id). 전략파라미터는 portfolio-hub 가 `strategy_params` JSONB 로 보관한다. trex-core daemon 의 universe = 자신이 관리하는 portfolio 집합이다. (근거: portfolio-hub `app/domain/models.py` Daemon/Portfolio, `strategy_params` JSONB migration)
- A-6: 이 trex-core daemon 은 자신이 관리하는 portfolio 의 유일한 매매 주체이며, 장중 거래 외 현금 변동(입출금)은 없다. 따라서 일일 baseline + 체결 시 portfolio-hub write 응답 갱신으로 현금/포지션을 정확히 유지할 수 있다(옵션 D 전제). (사용자 확인 2026-06-04)
- A-7 (확정 2026-06-05, portfolio-hub#8): portfolio-hub 가 `POST /v1/portfolios/{portfolio_id}/ledger/trades` 응답 root 에 flat 필드 **`cash_balance_after`, `asset_quantity_after`** 2개를 비파괴 추가하기로 회신. 더 나아가 이 endpoint 를 회계 단일 권위로 만들어 **ledger row INSERT + portfolio cash/quantity UPDATE 를 단일 DB 트랜잭션(row lock)** 으로 처리한다 → trex-core 는 write 1회로 회계 완결, drift 원천 차단, read round-trip 불요. status 별: `new`(201) = 본 거래 반영 후 권위 값, `duplicate`(200) = 현재 권위 값(idempotent 정합), `409`/잔고부족 `422` = `*_after` 미정의(본문 미정). trex-core 는 이 응답값으로 스냅샷 현금/포지션을 갱신한다(옵션 D). 의존: portfolio-hub 측은 별도 Discovery(dcy-006)·release(REL-003 등)로 배포 예정 — **배포 시점까지 trex-core 구현은 대기**. 추적: https://github.com/fourblocks2020/portfolio-hub/issues/8 . (사용자 결정 2026-06-05 / portfolio-hub 회신 반영)
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 스냅샷의 정의 — 응답 단위와 필드.
- 상태: Answered (2026-06-04)
- 결정: 스냅샷 = daemon 이 관리하는 portfolio universe. 항목당 symbol, asset_id(=portfolio_id), 현금(cash_balance), 포지션(asset_quantity), active_strategy_id, 전략파라미터(opaque JSON). 가격(price)은 사용하지 않으므로 제외. 전략파라미터는 portfolio-hub 가 `strategy_params` JSONB 로 보관하므로 trex-core 는 해석 없이 opaque JSON 으로 통과시킨다.
- 반영 위치: `# 3` 기대 상태, `# 5` FR-1·FR-4, `# 6` A-5.
- OQ-2: 신선도/캐시 정책 — universe 페치 주기와 현금/포지션 신선도.
- 상태: Answered (2026-06-04)
- 결정(개정 2026-06-05, 옵션 C → 옵션 D): universe 멤버십·전략파라미터는 설정 시각(기본 07:30 KST, .env) 일일 스냅샷으로 고정. 현금/포지션은 스냅샷 baseline 에 더해 **체결 시 portfolio-hub ledger write 요청의 응답이 반환하는 변경 후 권위 cash/position 으로 갱신**한다(옵션 D). 근거: portfolio-hub 가 cash/position 의 ground truth 이므로 write 응답값으로 덮어쓰면 옵션 C 의 core 로컬 체결 replay 누적 보정에서 생길 수 있던 drift(R-5)가 발생하지 않는다. 갱신이 체결(write) 경로에서만 일어나므로 read 경로의 portfolio-hub 가용성 결합(옵션 B 의 매 요청 live 호출)도 피한다. A-6(유일 매매 주체·장중 외부 변동 없음) 전제는 유지. 의존(확정 2026-06-05, portfolio-hub#8): portfolio-hub 가 ledger write 응답에 `cash_balance_after` / `asset_quantity_after` 를 반환하고 ledger INSERT + 잔고 UPDATE 를 단일 트랜잭션으로 처리하기로 회신 — trex-core 는 write 응답값으로 스냅샷을 갱신한다. portfolio-hub 배포(dcy-006/REL-003) 시점까지 구현 대기. 체결→write flush 사이 즉시 갭의 중복 매수 배제는 slot overlay(FR-2)가 계속 담당.
- 반영 위치: `# 4`, `# 5` FR-6·NFR-2·In Scope, `# 6` R-1·R-5·A-6·A-7.
- OQ-3: core 경유 정책 — 모든 소비자가 trex-core 를 경유하는가, 일부는 portfolio-hub 직접 호출을 허용하는가?
- 상태: Answered (2026-06-04)
- 결정: 매수 전략 모듈 등 **모든 소비자는 trex-core 스냅샷 API 를 경유**한다. portfolio-hub 직접 호출은 허용하지 않는다. core 가 단일 통합 경계(자격증명·계약·slot overlay·현금/포지션 보정·정합 검증)를 일관 제공하고, 소비자는 portfolio-hub 계약/자격증명을 알 필요가 없다.
- 반영 위치: `# 3` 제약사항, `# 6` R-4 완화.
- OQ-4: in-flight 합성 규칙 — portfolio-hub 포지션과 core 슬롯 상태를 합쳐 응답을 만들 때의 포함/마스킹 규칙.
- 상태: Answered (2026-06-05)
- 결정: 응답(전략 실행 대상 portfolio 자산 리스트)은 **portfolio-hub 보유 자산을 기본 포함하고 holding 슬롯 자산도 포함**한다. 다음 상태만 **마스킹**(제외): (a) 슬롯 pending(미체결 in-flight), (b) warning(브로커 실제 position 불일치, FR-7), (c) 사용자 pause(FR-9). 우선순위: core 권위 슬롯 상태 + 정합 검증 + pause 가 portfolio-hub 보유 여부보다 우선(마스킹 조건 충족 시 보유 여부와 무관하게 제외). in-flight race(R-2)는 pending 마스킹으로 차단; 체결 완료분은 holding 으로 포함. (종전 'pending·holding 동시 제외' → 'holding 포함, pending·warning·pause 마스킹'으로 변경.)
- 반영 위치: `# 3` 시나리오, `# 5` FR-2·FR-9, `# 6` R-2, `# 7` S-2.
- 잔여: holding 자산의 보유 수량 표기를 core 권위 슬롯 vs 스냅샷 중 무엇 기준으로 할지(holding→ledger flush 갭의 수량 정합)는 설계 시 확정. 종전 FR-3(가용 슬롯 0 → 빈 리스트)은 'holding 포함' 변경과 충돌 → OQ-11 로 재정의.
- OQ-5: 가용성 degrade 정책 — portfolio-hub 불가 시 스냅샷 read 동작.
- 상태: Answered (2026-06-05)
- 결정: portfolio-hub 가 불가해 **권위 cash/position 을 유지할 수 없는 상태**(옵션 D 의 ledger write `*_after` 갱신 불가, 또는 일일 universe fetch 실패)면 **신규 전략 매매를 보수적으로 중단**한다. 차단 사유는 'ledger 기록 불가'가 아니라(dcy-004 outbox 가 유실 없이 결국 기록) **stale cash/position 으로 매매 시 잘못된 사이징·누적 drift 위험**이다.
- read 응답: 빈 리스트(`[]` — '후보 없음'과 모호)나 silent stale 캐시 서빙 대신 **명시적 degraded 신호**를 실어 전략이 신규 매매를 보류하게 한다. 신호는 `degraded=true` + **degraded 사유**를 포함한다 — machine-readable reason code(예: `portfolio_hub_unavailable` / `snapshot_fetch_failed` / `cash_position_stale`)로 전략이 원인별 분기 가능하게 하고, 사람용 message 로 운영 진단을 돕는다. 단 raw 예외/자격증명 등 민감정보는 노출하지 않고 sanitize 한다.
- core 자율 보호 매도(손절·시간 만기, dcy-006 A-5)는 **계속 동작** — 그 체결의 ledger 도 outbox 가 복구 후 기록하므로 outage 중에도 열린 포지션은 보호된다. 즉 '중단'은 신규 전략 매매에 한정한다.
- dcy-006 OQ-4(RabbitMQ degrade)와 **공통 '보수적 안전' 철학**: 임의의 핵심 의존성(portfolio-hub OR RabbitMQ) degrade 시 신규 매매 중단 + core 보호 매도 유지.
- 반영 위치: `# 5` NFR-3, `# 6` R-3.
- 종료 조건(충족): degrade 동작이 NFR-3/R-3 에 반영됨.
- OQ-6: 빈 리스트 표현 시맨틱 — 응답이 비었을 때의 표현.
- 상태: Answered (2026-06-05)
- 결정: 응답은 **envelope** 로 감싸고 자산 목록은 항상 배열(`assets: []`, **null 금지**)로 준다. 빈 배열은 '항목 없음'을 그대로 의미하고, '왜 비었는지'는 envelope 의 meta 로 구분한다 — `view`(에코된 view 모드, OQ-11/12), `available_slots`(capacity 신호), `degraded`+`reason`(OQ-5). 근거: 리스트 필드에 null 을 섞으면 소비자 분기·버그 유발 → 항상 배열. 빈 리스트만으로는 capacity 0 / 전부 마스킹 / 필터 0매칭 / degrade 를 구분 못 하므로 envelope 필요(OQ-5 가 이미 `degraded`/`reason` 을 요구해 envelope 는 사실상 강제). transport 가 메시지 payload(dcy-006 RPC)라 REST status-code 가 아니라 구조화 envelope 가 자연스럽다.
- 반영 위치: `# 5` FR-3 (자산 항목(FR-1)·마스킹(FR-2)은 envelope 의 `assets[]` 원소로 위치).
- 종료 조건(충족): 표현 방식(envelope + `[]`)이 FR-3 에 반영됨. 정확한 envelope 필드 스키마는 OQ-12 view 파라미터와 함께 REQ/설계 단계 확정.
- OQ-7: trex-core daemon 의 `daemon_id` 출처.
- 상태: Answered (2026-06-05)
- 결정: `daemon_id` 는 **portfolio-hub 자격증명/인증 토큰(`auth_tokens.daemon_id`)에서 도출**한다(단일 진실원). 별도 `TREX_DAEMON_ID` .env 설정값을 1차 출처로 두지 않는다 — 자격증명과 분리 설정 시 dual-source drift(잘못된 universe·auth 오류) 위험. portfolio-hub read 는 토큰으로 daemon 을 스코프하므로(A-1) trex-core 는 daemon_id 를 명시적으로 들 필요가 가급적 없고, 값이 필요하면 startup 때 whoami(portfolio-hub `v1_user_info` 류) 1회로 도출·캐시한다. .env override 는 테스트/stub 한정이며 startup 에서 토큰 daemon_id 와 일치 검증(불일치 시 fail-closed)한다. dcy-004 의 `TREX_PORTFOLIO_HUB_*` 자격증명을 재사용하므로 신규 `TREX_DAEMON_ID` 주입은 불필요.
- 반영 위치: `# 4` 영향 범위(config), `# 5` FR-4.
- 종료 조건(충족): daemon_id 출처가 FR-4/영향 범위에 반영됨.
- OQ-8: 포지션 정합 검증 세부.
- 상태: Answered (2026-06-05)
- 결정:
1. **일치 기준 = 수량 정확 일치**(정수 shares, 허용 오차 없음). A-6(유일 매매 주체·장중 외부 변동 없음)상 portfolio-hub 포지션과 브로커 잔고는 동일 체결에서 파생되어 정확히 일치해야 하며, tolerance 는 실제 이상(누락 체결·외부 거래)을 가린다. 현금은 FR-7 범위 밖(수수료·세금·T+2 결제로 단순 비교 불가) — 수량만 대조.
2. **warning 회복 = 다음 정합 성공 시 자동 해제**(매 정합 사이클 재계산되는 파생 플래그, sticky 아님). raise 는 매번 로깅/alert. 영속 hold 가 필요하면 운영자 pause(FR-9)로 분리한다(자동 warning=일시 / 수동 pause=영속).
3. **브로커 포지션 출처 = KIS 주식잔고조회**(실전 `TTTC8434R` / 모의 `VTTC8434R`, `GET /uapi/domestic-stock/v1/trading/inquire-balance`)를 BrokerPort 잔고조회로 재사용. 항목당 `pdno`(종목코드) + `hldg_qty`(보유수량). paper broker 는 결정론 stub.
- **symbol 단위 집계 대조**: 브로커는 단일 계좌(dcy-003 불변식 B)라 잔고가 symbol 단위 집계 → portfolio-hub per-portfolio 포지션을 symbol 로 합산해 broker per-symbol `hldg_qty` 와 대조(보통 1 portfolio:1 symbol 이나 안전하게 집계 기준).
- 주의: 당일 전량매도분이 `hldg_qty=0` 으로 D-2 까지 잔고에 남을 수 있음(015 문서) → 0 수량 잔고 처리 규칙은 설계 시 확정.
- 반영 위치: `# 4` 영향 범위, `# 5` FR-7, `# 6` R-5.
- 참고: docs/broker/kis/rest/국내주식/주문/계좌/015_v1_국내주식-006_주식잔고조회.md
- 종료 조건(충족): 정합 기준·회복 규칙·포지션 출처가 FR-7 에 반영됨.
- OQ-9: 전략 ID 필터의 식별자/등록 시맨틱.
- 상태: Answered (2026-06-05)
- 결정:
- **식별자 형태**: 전략 모듈이 넘기는 `strategy_key` 는 **사람이 읽을 수 있으면서 ID 역할을 하는 유일·안정 값**(human-readable stable slug, 예: `target-value-v1`)이다. 'display name vs opaque ID' 이원화 없이 이 slug 자체가 유일 키다. 매칭은 대소문자 구분 정확 일치(exact).
- **명명(2026-06-05)**: 파라미터명을 `strategy_id` → **`strategy_key`** 로 변경한다. portfolio-hub 의 `strategy_id`(그쪽 내부 전략 엔티티 ID)와 이름이 겹쳐 혼동되므로 분리한 것이다. `strategy_key` 는 trex-core 요청 파라미터(전략 모듈 자기 식별자)이며, 그 **값은 자산의 portfolio-hub `active_strategy_id` 와 매칭**된다.
- **등록 판정**: 자산의 **단일 `active_strategy_id` 와 동등 비교**(`strategy_key == active_strategy_id`). 한 자산은 한 시점에 하나의 active 전략에 귀속한다(portfolio-hub `active_strategy_id` 단일, A-5). 자산당 다중 전략 등록(예: buy/sell 분리, trex-core#1)은 **현재 범위 밖**이며 필요해지면 '등록 목록' 필드로 확장(후속).
- 반영 위치: `# 5` FR-8, `# 3` 시나리오.
- 종료 조건(충족): 식별자 형태·등록 판정이 FR-8 에 반영됨.
- OQ-10: 런타임 pause/resume 메커니즘.
- 상태: Answered (2026-06-05)
- 결정:
- **단위**: `portfolio_id`(= asset_id) — universe 항목 키. (symbol 은 다중 portfolio 공유 시 모호하므로 portfolio_id 기준.)
- **제어 방식**: 운영자 admin 액션으로 **CLI + dcy-006 RPC 제어 메시지** 둘 다 제공(로컬 CLI + 원격 ops). config 리로드는 비채택.
- **영속성**: **Postgres 영속**(기존 단일 store), portfolio_id 키. daemon 재시작 후 유지, 일일 스냅샷 refresh 와 무관(refresh 는 universe/포지션만 갱신, pause overlay 불변).
- **적용 범위**: **전역** — 모든 전략 요청에서 마스킹(운영자 안전 액션, 어느 전략이 묻든 거래 금지).
- **resume**: pause 는 request-time mask 라 resume 시 **다음 요청부터 즉시 응답 복귀**(일일 갱신 대기 불요).
- pause(수동·영속) vs warning(자동·일시, OQ-8)은 독립 overlay 이며 OR 합성(둘 중 하나라도면 마스킹).
- 반영 위치: `# 4` 영향 범위, `# 5` FR-9.
- 종료 조건(충족): pause 단위·제어 방식·영속성·적용 범위가 FR-9 에 반영됨.
- OQ-11: capacity 소진 시 응답 시맨틱.
- 상태: Answered (2026-06-05) — 방향 확정, 파라미터 설계는 OQ-12 로 분리.
- 결정: capacity(가용 슬롯)가 모두 소진됐을 때의 응답은 **고정값이 아니라 전략 모듈이 요청 시 지정하는 view 모드(파라미터)에 따라 달라진다.** 최소 3개 모드: (a) **빈 리스트 `[]`** (신규 매수 후보가 목적인 전략 — 살 게 없음), (b) **슬롯 점유 자산만** (보유 포지션 관리·매도/조절 전략 — 자기가 들고 있는 것만), (c) **portfolio 스냅샷 − 슬롯 점유 자산** (점유 안 된 잔여 universe). 같은 view 모드 개념은 capacity 0 이 아닐 때도 응답 구성을 좌우한다(이 파라미터는 strategy_key 필터(FR-8)와 별개의 두 번째 요청 파라미터). (→ OQ-12 에서 enum `all`/`buyable`/`held` 으로 확정.)
- 반영 위치: `# 5` FR-2·FR-3, `# 6` OQ-12.
- 잔여(→ OQ-12): 파라미터 이름/enum 값, 기본 모드(생략 시), 각 모드와 마스킹(pending·warning·pause, OQ-4)·strategy_key 필터의 합성 규칙, capacity 신호 메타 필드(OQ-6) 동반 여부.
- OQ-12: 응답 view/scope 파라미터 설계.
- 상태: Answered (2026-06-05)
- 결정:
- **파라미터**: 요청 payload `view`(dcy-006 OQ-2 매핑), enum = **`all` | `buyable` | `held`**. base 집합 — `all` = universe 전체(portfolio-hub 보유 + holding 슬롯), `buyable` = universe − 슬롯 점유(미점유 신규 진입 후보), `held` = holding 슬롯 자산만. (1차안의 `remainder` 는 `buyable` 과 동일이라 drop.)
- **기본 모드**: 생략 시 **`all`**(OQ-4/FR-2 holding 포함·FR-8 전체와 정합).
- **마스킹 직교·균일**: 모드는 base 집합만 고르고, 마스킹(pending·warning·pause)은 전 모드 균일 적용. **pending 은 모든 모드 마스킹** → `held` = holding 만(미체결 제외). warning/pause 도 전 모드 마스킹.
- **strategy_key 직교**: `view` 와 `strategy_key`(FR-8)는 독립 합성 — `assets = base(view) − mask(pending·warning·pause) − (strategy_key 필터, 주어지면)`.
- **capacity**: envelope 에 `available_slots` 항상 포함(OQ-6). `buyable` 은 capacity-gated — `available_slots=0` 이면 `[]`(OQ-11 정합). `held` 엔 정보성.
- **전략 타입 매핑(trex-core#1)**: `buyable`=매수, `held`=position/매도, `all`=통합.
- 반영 위치: `# 5` FR-2·FR-3·FR-8.
- 종료 조건(충족): 파라미터 스펙·기본 모드·합성 규칙이 FR-2/FR-3/FR-8 에 반영됨.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: stub portfolio-hub read 로, 스냅샷 API 가 portfolio-hub 종목을 포함한 전략 실행 대상 자산 리스트를 반환한다.
- S-2: holding 슬롯 자산은 응답에 포함되고, pending 슬롯 자산은 마스킹되어 제외된다(slot overlay).
- S-3: 가용 슬롯 0 일 때 `view=all`/`held` 는 보유 자산을 계속 반환하고 `view=buyable` 은 `[]` 를 반환하며, `available_slots=0` meta 가 신호된다(OQ-11/OQ-12).
- S-4: 도메인 import 스윕에서 portfolio-hub/네트워크 의존이 0 으로 유지된다.
- S-5: interface-contract/runtime-flows/project-structure 가 read 레이어와 정합한다.
- S-6: 스냅샷 페치 시 portfolio-hub 포지션과 브로커 계좌 포지션이 불일치하는 자산이 `warning` 으로 표시되고 응답 리스트에서 마스킹된다.
- S-7: `strategy_key` 를 넣어 호출하면 해당 `strategy_key` 가 등록된 자산만, 생략하면 전체 자산이 반환된다.
- S-8: 자산을 런타임에 pause 하면 응답 리스트에서 마스킹되고, resume 하면 다시 포함된다.
- S-9: portfolio-hub 가 불가해 권위 cash/position 유지가 불가하면 응답에 `degraded=true` + reason code 가 실려 신규 매매 보류를 신호한다(OQ-5).
- S-10: 요청 `view`(`all`/`buyable`/`held`)별로 base 집합이 다르고 마스킹(pending·warning·pause)은 전 모드 균일 적용된다(OQ-12).
- S-11: 응답이 `assets`(항상 배열, 비면 `[]`·null 아님) + meta(`view`·`available_slots`·`degraded`·`reason`) envelope 형태다(OQ-6).
- S-12: 별도 `TREX_DAEMON_ID` 없이 portfolio-hub 토큰에서 도출한 daemon_id 로 universe 를 조회한다(OQ-7).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: stub read 통합테스트(스냅샷 응답) / 데이터 출처: 테스트 결과, 응답 본문.
- S-2 측정 방식: 슬롯 holding/pending 진입 후 스냅샷 호출 테스트 / 데이터 출처: 응답 리스트(holding 포함·pending 마스킹) vs 슬롯 상태.
- S-3 측정 방식: 슬롯 소진(capacity 0) 후 스냅샷 호출 테스트 / 데이터 출처: 응답 리스트·capacity 신호 메타(OQ-11 확정안 기준).
- S-4 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain` import 목록.
- S-5 측정 방식: 문서-코드 정합 점검(리뷰) / 데이터 출처: baseline 문서 vs 실제 메시지 계약(RPC)/read 포트.
- S-6 측정 방식: 포지션 불일치 주입 후 스냅샷 페치 테스트 / 데이터 출처: 자산 `warning` 상태·응답 리스트.
- S-7 측정 방식: `strategy_key` 유/무 호출 비교 테스트 / 데이터 출처: 응답 리스트 vs 자산 `active_strategy_id`.
- S-8 측정 방식: pause→호출→resume→호출 시퀀스 테스트 / 데이터 출처: 응답 리스트 포함/제외 여부.
- S-9 측정 방식: portfolio-hub 불가(권위 유지 불가) 주입 후 호출 테스트 / 데이터 출처: 응답 `degraded`·`reason` 필드.
- S-10 측정 방식: `view` 값별 호출 비교 테스트 / 데이터 출처: 응답 base 집합 vs 슬롯 상태·마스킹.
- S-11 측정 방식: 응답 스키마 검증(빈 응답 포함) / 데이터 출처: 응답 envelope 구조(`assets` 타입·meta 필드).
- S-12 측정 방식: `TREX_DAEMON_ID` 미설정 기동 후 universe 조회 테스트 / 데이터 출처: 토큰 도출 daemon_id vs 조회된 portfolio 집합.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — 특히 전략 의사결정 로직·reconciliation 제외
- 기준: 운영자가 read 슬라이스 경계를 확인했다.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-12 측정 방식·출처 명시(본 문서 # 7).
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 본 Discovery 의 리스크는 현재 전부 Medium/Low. 신선도(R-1)·race(R-2)·가용성(R-3) 처리 방침을 운영자가 확인.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-12 이 전부 Answered/Deferred 처리(현재 OQ-1~OQ-12 전부 Answered).
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md
- 참조 문서:
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md (write 방향 — 본 Discovery 가 read 방향으로 이어받음)
- docs/discovery/dcy-006_20260605_strategy-module-rabbitmq-transport.md (confirmed — 스냅샷 read 의 전달 transport(RabbitMQ RPC, HTTP 0). 본 Discovery 는 logical 계약, dcy-006 은 transport)
- docs/interface-contract.md, docs/runtime-flows.md, docs/project-structure.md (baseline — 본 Discovery 가 갱신 요구)
- trex-orbit `app/clients/portfolio_client.py` (get_assets/get_portfolios read 계약 참고)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~스냅샷 응답 단위/필드~~ → 결정됨: daemon 관리 portfolio universe(symbol/asset_id/현금/포지션/active_strategy_id/전략파라미터 opaque JSON), 가격 제외 (OQ-1, 2026-06-04)
- ~~신선도 정책~~ → 결정됨: universe·전략파라미터 설정 시각 일일 캐시 + 현금/포지션 baseline + 체결 시 portfolio-hub write 응답의 권위 cash/position 갱신(옵션 D) (OQ-2, 2026-06-04 / 옵션 C→D 개정 2026-06-05)
- ~~daemon_id 주입~~ → 결정됨: portfolio-hub 자격증명/토큰에서 도출(단일 진실원), 별도 `TREX_DAEMON_ID` 불필요; .env override 는 테스트 한정 + 토큰 일치 fail-closed (OQ-7, 2026-06-05)
- ~~빈 표현 시맨틱~~ → 결정됨: envelope + `assets: []`(null 금지), meta(`view`·`available_slots`·`degraded`·`reason`)로 의미 분리 (OQ-6, 2026-06-05)
- ~~capacity 0 시 응답 재정의~~ → 결정됨: 응답 형태를 전략 모듈 요청 view 모드 파라미터로 선택(빈 `[]` / 슬롯 점유 자산만 / portfolio−점유) (OQ-11, 2026-06-05)
- ~~응답 view/scope 파라미터~~ → 결정됨: `view`=`all`(기본)|`buyable`|`held`, strategy_key 직교 합성, 마스킹 전 모드 균일(`held`=holding만), `available_slots` 메타(buyable은 0이면 `[]`) (OQ-12, 2026-06-05)
- ~~전략 ID 시맨틱~~ → 결정됨: 사람이 읽는 안정 유일 slug(예: `target-value-v1`), 자산의 단일 `active_strategy_id` 와 정확 일치; 다중 등록은 범위 밖 (OQ-9, 2026-06-05)
- ~~pause/resume 메커니즘~~ → 결정됨: portfolio_id 단위, CLI + dcy-006 RPC 제어, Postgres 영속(refresh 무관), 전역, 즉시 복귀 (OQ-10, 2026-06-05)
## CONFIRM
- ~~read 슬라이스 범위 경계~~ → 확정됨(2026-06-06, Justin): In Scope(portfolio-hub read 포트/어댑터·스냅샷 read 계약·마스킹·view 모드·옵션 D·정합 검증·baseline) / Out of Scope(전략 의사결정 로직·reconciliation·safety floor) 확인
- ~~core 경유 정책~~ → 결정됨: 모든 소비자 trex-core 경유, portfolio-hub 직접 호출 금지 (OQ-3, 2026-06-04)
- ~~in-flight 합성/제외 규칙~~ → 결정됨: holding 포함, pending·warning·pause 마스킹, core 권위 우선 (OQ-4, 2026-06-05)
- ~~가용성 degrade 정책~~ → 결정됨: 권위 cash/position 유지 불가 시 명시적 degraded 신호 + 신규 매매 보수적 중단, core 보호 매도 유지 (OQ-5, 2026-06-05). dcy-006 OQ-4 와 공통 보수적 안전.
## DATA
- ~~portfolio-hub read 계약 확인~~ → 검증됨(2026-06-06, confirm-discovery): portfolio-hub `app/api/v1_portfolios.py` `list_portfolios_v1`(`GET`)이 `managed_by_daemon_id=auth.daemon_id` 로 daemon universe 스코프 조회를 제공하고, `PortfolioItem` 에 `cash_balance`·`asset_quantity`·`managed_by_daemon_id` 포함. 전략파라미터는 `strategy_params` JSONB(A-5).
- ~~portfolio-hub write 응답 계약 확장~~ → 확정됨(2026-06-05, portfolio-hub#8): `POST /v1/portfolios/{portfolio_id}/ledger/trades` 응답 root 에 flat 필드 `cash_balance_after`, `asset_quantity_after` 추가 + ledger INSERT/잔고 UPDATE 단일 트랜잭션(회계 단일 권위). status 별 `*_after` 의미 정의(new=반영 후, duplicate=현재 권위, 409/422=미정). portfolio-hub 측은 dcy-006/REL-003 으로 배포 예정 — 배포 시점까지 trex-core 구현 대기. 추적: https://github.com/fourblocks2020/portfolio-hub/issues/8 (옵션 D 의존, A-7)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1(스냅샷 정의)·OQ-2(신선도, 옵션 D — portfolio-hub#8 write 응답 `*_after` 확정)·OQ-3(core 경유 정책)·OQ-4(in-flight 합성 — holding 포함, pending·warning·pause 마스킹)·OQ-5(가용성 degrade — 권위 cash/position 유지 불가 시 degraded 신호+보수적 중단)·OQ-6(빈 표현 — envelope + `assets: []`/meta)·OQ-7(daemon_id — 토큰 도출 단일 진실원)·OQ-8(정합 — 수량 정확 일치·symbol 집계·자동 해제·KIS 잔고조회)·OQ-9(전략 ID — 사람이 읽는 안정 slug·단일 active 일치)·OQ-10(pause — portfolio_id·CLI+MQ·Postgres 영속·전역·즉시 복귀)·OQ-11(capacity 0 응답 — view 모드 파라미터)·OQ-12(view 파라미터 — `all`/`buyable`/`held` + envelope) **전부 Answered**. 남은 인계 전 항목은 범위 경계 확인(운영자)과 CONFIRMED_BY 지정이며, confirm-discovery 에서 확정 후 REQ Drafting 으로 인계한다.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-06 00:08
@@ -1,279 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-006_20260605_strategy-module-rabbitmq-transport
- 이슈명: trex-core ↔ 전략 모듈 통신 transport — RabbitMQ 도입 (스냅샷 read + intent 제출 + 이벤트)
- 작성 시각(KST): 2026-06-05 21:23
- 마지막 갱신 시각(KST): 2026-06-05 22:45
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-021, REQ-022, REQ-023, REQ-024, REQ-025, REQ-026
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: dcy-005(portfolio-hub 종목 스냅샷 read API — FR-1/A-3 가 HTTP pull 가정), dcy-002(slot-core 데몬 + HTTP intent API), dcy-003(주문 집행 + durable outbox/idempotency), trex-core#1(매수/매도/position 전략 taxonomy)
- 해석:
- dcy-002(intent API)와 dcy-005(스냅샷 read)는 전략 모듈과의 통신을 **HTTP API**로 가정했는데, 사용자가 전략 모듈 통신을 API endpoint 대신 **RabbitMQ(AMQP)** 로 하고자 한다. transport 선택은 스냅샷 read·intent 제출·이벤트 전반에 걸친 cross-cutting 통합 아키텍처 결정이라, read 슬라이스(dcy-005) 안에 두지 않고 **독립 Discovery로 분리**한다.
- 핵심 분리 원칙: logical 계약("무엇을 주고받나" — 스냅샷 필드/마스킹/view 모드/strategy_id 필터, intent 스키마)은 dcy-005/002/003 소관으로 **transport-무관**하게 유지하고, 본 Discovery 는 "어떻게 전달하나"(전달 방식·메시지 매핑·delivery 보증·운영)만 다룬다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- transport/messaging/RabbitMQ 를 다루는 기존 Discovery 가 없다(dcy-001~005 탐색 결과, repo 내 amqp/rabbitmq/pika 구현 0건).
- 가장 가까운 dcy-002 는 HTTP API "수립"이 주제이지 transport "선택"이 아니며, MQ 전환은 dcy-002/005 의 미세 조정이 아니라 통신 모델 자체를 바꾸는 새 통합 아키텍처 문제다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- trex-core daemon 은 FastAPI/uvicorn 기반 **HTTP `/v1` API**(`trex_core/adapters/api/app.py`)로 전략/소비자와 통신한다(`/v1/intents/buy|sell`, `/v1/slots`, `/v1/capacity`; dcy-005 스냅샷 read 는 draft). messaging/AMQP 인프라는 없다(repo grep: rabbitmq/amqp/pika 0건).
- baseline 문서가 HTTP 를 명시적 transport 로 기술한다 — `interface-contract.md`: "HTTP `/v1` API :: 슬롯 코어 데몬이 노출, 매수·매도 전략 데몬이 호출". `runtime-flows.md` 도 HTTP request/response 흐름 기준.
- 전략 모듈은 외부 소비자로서 HTTP pull 로 스냅샷을 받고 HTTP POST 로 intent 를 제출한다고 가정한다(dcy-005 A-3, dcy-002).
- adapters 레이어는 `api / broker / ledger / persistence` 로 구성되며 messaging 어댑터는 없다.
- 기대 상태
- trex-core ↔ 전략 모듈 통신을 RabbitMQ(AMQP) 메시지 기반으로 제공한다. 스냅샷 요청과 intent admission 은 request-reply(reply-to/correlation-id), 체결/슬롯 상태 변화는 이벤트(pub/sub)로 전달한다(intent 체결 결과는 이벤트로, OQ-2).
- view 모드(dcy-005 OQ-11)·strategy_id(dcy-005 FR-8) 같은 요청 속성이 메시지 속성/routing key 로 매핑된다.
- logical 계약은 불변이고 transport 만 교체된다. HTTP `/v1` API 는 RabbitMQ 로 완전 대체되어 **HTTP 표면이 0 이 된다**(병행 없음, OQ-1; healthz 포함 제거, OQ-7). liveness 는 프로세스 감독이 담당.
- 이해관계자
- 문서 작성자/구현자(AI agent)
- 리뷰어/승인자(운영자, genichin@gmail.com)
- 전략 모듈(trex-core 외부 소비자) 및 trex-core daemon
- 운영자(RabbitMQ 인프라 도입·운영)
- 제약사항
- 도메인 순수성 — `trex_core.domain` 은 transport/AMQP 의존 0(어댑터 격리).
- logical 계약 보존 — 스냅샷/intent 의 의미·필드·마스킹·view 모드·스키마는 transport 와 독립이며 본 Discovery 가 바꾸지 않는다.
- core 권위 불변식 유지 — 슬롯/capacity 단일 writer(불변식 A) 는 transport 교체와 무관하게 보존된다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략 모듈이 portfolio 스냅샷을 요청 → trex-core 가 RabbitMQ reply 큐로 전략 실행 대상 자산 리스트를 응답한다(HTTP GET 대체).
- 전략 모듈이 매수/매도(향후 target) intent 를 메시지로 발행 → trex-core 가 소비해 슬롯 전이를 수행한다.
- 체결/슬롯 상태가 바뀌면 → trex-core 가 이벤트를 발행하고 전략 모듈이 구독해 즉시 반영한다(HTTP polling 불요).
- RabbitMQ 가 일시 불가 → degrade 정책(버퍼링 금지·신규 매매 보수적 중단, OQ-4)에 따라 신규 전략 매매는 멈추고, 열린 포지션은 core 자율 안전 매도가 보호한다(미구현, A-5).
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- transport 추상화 + RabbitMQ 어댑터(`pika` 동기): trex-core ↔ 전략 통신을 메시지 기반으로 제공. 스냅샷·intent admission = request-reply, 이벤트 = pub/sub 패턴 정의. 어댑터는 `adapters/messaging`(신규) 레이어로 격리하고 결정론 stub 제공. 런타임은 uvicorn 제거 후 sync+thread daemon 재구성(OQ-5).
- 메시지 계약 매핑: 기존 logical 계약(스냅샷 필드/마스킹/view 모드/strategy_id, intent 스키마)을 메시지 payload/속성/routing key 로 1:1 의미 보존 매핑.
- delivery 신뢰성: ack/재처리, idempotency(dcy-003 의 intent correlation_id 재사용), dead-letter, 내구성 큐. 순서는 intent 단일 consumer FIFO 로 보존하고 이벤트는 순서 보증 불요(상태 운반·last-wins, OQ-3).
- HTTP 완전 제거: HTTP `/v1` API 를 RabbitMQ 로 대체(병행 없음, OQ-1)하고 **`healthz` 포함 모든 HTTP 라우트와 `adapters/api` HTTP 어댑터를 제거**(HTTP 표면 0, OQ-7). dcy-005 FR-1/A-3·dcy-002 의 HTTP 가정을 갱신. liveness 는 프로세스 감독, 앱 헬스/관측성은 로그·메트릭.
- baseline 갱신: `interface-contract.md`(메시지 계약)·`runtime-flows.md`(MQ 흐름)·`project-structure.md`(messaging 어댑터) 갱신.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- 신규: `trex_core/adapters/messaging` RabbitMQ(`pika`) 어댑터 + stub, 스레드별 connection/channel 관리, 직렬화, request-reply correlation(reply-to/correlation-id), intent RPC consumer 스레드, snapshot RPC consumer 스레드, 이벤트 publisher, 재연결/백오프, `config` 에 `TREX_RABBITMQ_URL`(.env) 주입(기존 `TREX_*` 패턴, OQ-6). uvicorn 제거로 `TREX_API_HOST`/`TREX_API_PORT` 폐기.
- 기존: **`adapters/api` HTTP 어댑터·uvicorn 제거**, daemon entrypoint 를 sync+thread 런타임으로 교체(OQ-5), application 레이어의 intent/스냅샷 진입점을 transport-무관 포트로 재배선(직접 변경 최소화).
- 문서 영향:
- interface-contract/runtime-flows/project-structure 갱신, dcy-005 FR-1/A-3·dcy-002 HTTP 가정 완화 cross-reference.
- 운영 영향:
- RabbitMQ 브로커 인프라 추가(현 스택의 redis/timescale 처럼 docker 추가 가능). exchange/queue 토폴로지, 모니터링, 자격증명/접속 운영.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: trex-core ↔ 전략 모듈 통신을 RabbitMQ(AMQP) 메시지 기반으로 제공한다(토폴로지 OQ-2). 스냅샷 요청과 **intent admission 은 request-reply**(`trex.snapshot.req`/`trex.intent.req` + RabbitMQ direct reply-to + `correlation_id`), 체결/슬롯 상태 변화는 **이벤트(topic exchange `trex.events`, pub/sub)** 로 전달한다. intent 의 체결 결과는 admission 응답이 아니라 이벤트로 전달해 현 2단계 시맨틱(admission 동기 + fill 비동기)을 보존한다.
- FR-2: 기존 logical 계약(스냅샷 필드/마스킹/view 모드(dcy-005 OQ-11)/strategy_id 필터(dcy-005 FR-8), intent 스키마(dcy-002/003))을 transport-무관으로 보존하고, 메시지 payload/속성/routing key 로 의미 손실 없이 매핑한다. 매핑 규칙(OQ-2): **요청(스냅샷·intent)의 view 모드·strategy_id 는 payload, 이벤트의 strategy_id·symbol 은 routing key**(`{event_type}.{strategy_id}.{symbol}`)로 싣는다.
- FR-3: messaging 접근을 transport 포트 추상화 + RabbitMQ 어댑터(`pika` 동기 클라이언트) + 결정론 stub 로 제공한다. 런타임은 uvicorn 제거 후 순수 sync+thread daemon(consumer/execution/ledger 스레드)으로 재구성한다(OQ-5). `trex_core/domain` 은 transport/AMQP 의존 0 을 유지한다.
- FR-4: delivery 신뢰성 — at-least-once + idempotency 로 effectively-once 를 제공한다(OQ-3). intent 는 슬롯 durable commit 후 ack + `correlation_id`(dcy-003 멱등키) dedup, durable/persistent + 단일 consumer FIFO. 이벤트는 durable exchange + per-strategy durable queue(catch-up) + 현재 상태·단조 시퀀스 운반(last-wins, 순서 보증 불요) + TTL/max-length. 발행은 publisher confirms, 실패는 dead-letter. 중복 전달에도 슬롯 단일 writer 불변식을 유지하고, 미수신/중복은 스냅샷 RPC 로 reconcile 한다.
- FR-5: HTTP `/v1` API 를 RabbitMQ 로 **완전 대체**한다(병행 없음, OQ-1; HTTP 표면 0, OQ-7). 전략-facing 통신(`intents/buy|sell`·`intents/{id}`·`slots`·`capacity`·dcy-005 스냅샷 read)을 메시지 기반으로 이전하고, **`healthz` 포함 모든 HTTP 라우트와 HTTP 어댑터를 제거**한다. liveness 는 프로세스 감독이 담당(앱 헬스/관측성은 로그·메트릭). dcy-005 FR-1/A-3·dcy-002 의 HTTP 가정을 갱신한다.
- FR-6: baseline 문서(`interface-contract.md`/`runtime-flows.md`/`project-structure.md`)를 메시지 계약·MQ 흐름·messaging 어댑터에 맞게 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 도메인 순수성 — transport/AMQP 의존은 adapters 레이어에 격리하고 `trex_core/domain` import 0(import 스윕).
- NFR-2: 신뢰성/일관성 — at-least-once 전달 + idempotency 로 effectively-once 효과를 보장한다(OQ-3, 진짜 exactly-once 는 미추구). 슬롯/capacity 권위 불변식을 보존하고, 이벤트는 advisory(push 최적화)이며 스냅샷 RPC 가 권위 reconcile 경로다.
- NFR-3: 가용성 degrade — RabbitMQ 불가 시 **버퍼링 금지 + 신규 매매 보수적 중단**(OQ-4). 단 **core 자율 안전 매도(손절·시간 만기)는 MQ·전략과 독립으로 계속**해 열린 포지션을 보호한다(이 capability 는 별도 core 기능, 현재 미구현 — A-5). dcy-005 OQ-5 와 공통 '보수적 안전' 철학을 따른다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- transport 포트 + RabbitMQ 어댑터 + stub, 스냅샷·intent admission request-reply, 이벤트 pub/sub, 메시지 계약 매핑, delivery 신뢰성, HTTP 완전 제거, baseline 갱신.
- Out of Scope (다른 Discovery 소관)
- 스냅샷/intent 의 logical 계약 자체 변경(dcy-005/002/003), 전략 의사결정 로직(전략 모듈 책임, trex-core#1), RabbitMQ 클러스터/HA 운영 세부, portfolio-hub 등 다른 외부 연동의 transport.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Low, 완화됨): 이중 transport 복잡도 — OQ-1 에서 HTTP 병행 없이 **대체**로 결정되어 계약 표류·중복 유지 비용은 대부분 해소. 단 전환기 동안 HTTP 어댑터 제거 시점과 전략 모듈 MQ 전환의 동기화가 필요(big-bang 마이그레이션 리스크는 R-5 로 분리).
- 완화 방안: 단일 logical 계약을 transport-무관으로 정의(FR-2/FR-3), HTTP 어댑터 제거를 전략 모듈 전환과 함께 단계적 계획.
- R-2 (Medium): delivery 의미 불일치 — at-least-once 재전달이 intent 를 중복 실행할 수 있다.
- 완화 방안(OQ-3 Answered): dcy-003 correlation_id idempotency dedup + 슬롯 durable commit 후 ack + dead-letter(FR-4), 슬롯 단일 writer 불변식(NFR-2). 미수신/중복 이벤트는 스냅샷 RPC reconcile.
- R-3 (Medium→High, 대체 결정으로 상향): 가용성 결합 — RabbitMQ 장애가 전략 통신을 전면 차단한다. HTTP 병행을 두지 않기로(OQ-1) 했으므로 **HTTP fallback 이 없어 단일 장애점 성격이 강해진다.**
- 완화 방안(OQ-4 Answered): 버퍼링 금지 + 신규 매매 보수적 중단. **열린 포지션은 core 자율 안전 매도(손절·시간 만기)가 MQ·전략과 독립으로 보호** → SPOF 영향이 '신규 매매 중단'으로 한정되고 포지션 방치 위험은 제거된다. 단 이 보호는 아직 미구현 safety 기능에 의존(A-5, 추후 구현). RabbitMQ durable/HA 운영도 고려.
- R-4 (Low): request-reply over MQ 복잡도 — 동기 HTTP pull 보다 reply 상관·타임아웃 처리가 필요하다.
- 완화 방안: correlation-id + reply-to 규약(OQ-2). RPC per-request 타임아웃·재요청 규약은 설계 시 표준화(전략 클라이언트 측).
- R-5 (Medium): big-bang transport 마이그레이션 — HTTP 제거와 전략 모듈 MQ 전환이 어긋나면 통신 단절.
- 완화 방안: 전환 순서/롤백 계획 수립, stub 기반 사전 검증(FR-3), 전환 중 관측성은 로그·메트릭으로 확보(OQ-7 에서 HTTP healthz 제거 결정 — probe 대체 안 함).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 전략 모듈은 RabbitMQ 클라이언트로 메시지를 발행/구독할 수 있는 외부 소비자다.
- A-2: trex-core 는 슬롯/capacity 권위 단일 소스이며 transport 교체와 무관하게 그 불변식을 유지한다.
- A-3: 스냅샷/intent 의 logical 계약(필드/마스킹/view 모드/strategy_id/스키마)은 dcy-005/002/003 에서 정의되고, 본 Discovery 는 전달 방식만 다룬다.
- A-4: RabbitMQ 브로커는 운영 환경에 도입 가능하다(현 스택에 redis/timescale 처럼 docker 로 추가 가능).
- A-5: trex-core 는 transport·전략과 독립으로 동작하는 **기본 안전 매도(손절 stop-loss, 시간 만기 forced_close) capability 를 갖는다(또는 추후 구현)**. degrade 시(RabbitMQ 불가·전략 슬롯 방치) 열린 포지션 보호의 전제다(OQ-4). 현재 미구현(`forced_close_at` 필드만 존재, stop-loss 없음)이며 구현은 별도 작업으로 추적(transport 범위 밖 core 매매 안전 기능).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: HTTP 대체 vs 병행 — RabbitMQ 가 HTTP `/v1` API 를 완전 대체하는가, 둘을 병행(이중 transport)하는가?
- 상태: Answered (2026-06-05)
- 결정: **RabbitMQ 가 HTTP `/v1` API 를 대체한다.** 이중 transport 병행은 채택하지 않는다. 전략-facing 통신(`intents/buy|sell`, `intents/{id}`, `slots`, `capacity`, dcy-005 스냅샷 read)은 전부 메시지 기반(command / request-reply / event)으로 옮긴다.
- 반영 위치: `# 3` 기대 상태, `# 4` 변경 항목, `# 5` FR-5, `# 6` R-1·R-3.
- 잔여(해소): 운영 probe/admin 표면의 '대체' 범위는 OQ-7 에서 Answered(앱 레벨 헬스 표면 제거, HTTP 표면 0).
- OQ-2: 통신 패턴별 토폴로지 — exchange/queue/routing key + reply 상관 + view 모드/strategy_id 매핑.
- 상태: Answered (2026-06-05)
- 결정: 3 패턴 토폴로지 확정.
- ① 스냅샷 RPC(request-reply): 요청 queue `trex.snapshot.req`(direct/default exchange) → trex-core 소비, 응답은 RabbitMQ direct reply-to(`amq.rabbitmq.reply-to`) + `correlation_id`. 요청 payload 에 `strategy_id`·`view` 모드.
- ② intent admission RPC(request-reply): 요청 queue `trex.intent.req` → trex-core 소비, 응답 = admission(`status`, `slot_id`, `reason`). `correlation_id` = intent 멱등키(dcy-003 `{asset_id}:{order_id}:{execution_id}`)로 RPC 상관 + dedup 일원화. **체결/종료(fill/close) 결과는 ② 응답이 아니라 ③ 이벤트로** 전달 — 현 2단계 시맨틱(admission 동기 + fill 비동기) 무손실 보존.
- ③ 이벤트 pub/sub: trex-core 가 체결·슬롯 상태 변화를 topic exchange `trex.events` 에 발행, routing key = `{event_type}.{strategy_id}.{symbol}`. 전략은 자기 queue 를 `*.{strategy_id}.#` 패턴으로 바인딩해 자기 자산 이벤트만 수신(broker단 필터).
- 매핑 원칙: view 모드·strategy_id 는 **요청(①②)에서는 payload, 이벤트(③)에서는 routing key**(요청은 라우팅 대상이 아니고 이벤트는 대상).
- 반영 위치: `# 5` FR-1·FR-2·FR-4.
- 잔여(→ 타 OQ): 이벤트 queue durability(전략 down 중 유실)·at-least-once/exactly-once → OQ-3; RPC reply 대기 클라이언트(pika/aio-pika) → OQ-5; RPC 타임아웃/degrade → OQ-4.
- OQ-3: delivery 보증 수준 — at-least-once + idempotency 로 충분한가, 추가 ordering/exactly-once 가 필요한가?
- 상태: Answered (2026-06-05, 위임 결정)
- 결정: **at-least-once + idempotency = effectively-once** 를 기준으로 한다. 분산 메시징에서 진짜 exactly-once 는 불가하므로 추구하지 않고 멱등 소비로 '사실상 한 번' 효과를 낸다. 패턴별:
- ① 스냅샷 RPC: read-only → 멱등. at-least-once, 별도 dedup 불요. 요청/응답은 transient(클라이언트 timeout 후 재요청). manual ack.
- ② intent admission RPC: at-least-once + `correlation_id`(= dcy-003 멱등키) dedup. 슬롯 전이를 Postgres 에 durable commit 한 뒤 ack → crash 로 재전달돼도 idempotency 가 중복 실행을 차단. intent 요청 queue·메시지는 durable/persistent, **단일 consumer FIFO** 로 순서 보존(trex-core 단일 writer 불변식 A 와 정합). 전역 total order 는 불요.
- ③ 이벤트 pub/sub: at-least-once + 멱등 소비. durable exchange + **per-strategy durable queue 로 전략 down 중 이벤트 catch-up**. 이벤트는 delta 가 아니라 **현재 상태 + 단조 증가 시퀀스(슬롯 version/timestamp)** 를 실어 last-wins 적용 → **순서 보증 불요**. 미수신/중복은 **스냅샷 RPC(권위 reconcile 경로)** 로 항상 복구(이벤트는 push 최적화이지 단일 진실원이 아님).
- 공통: trex-core 발행은 publisher confirms 로 broker 수락 확인. per-strategy 이벤트 queue 는 무한 적체 방지를 위해 TTL/max-length(초과분 drop-oldest, 스냅샷 reconcile 로 회복) 적용.
- 반영 위치: `# 5` FR-4·NFR-2, `# 6` R-2.
- 근거: 멱등키(dcy-003)와 스냅샷 권위 reconcile 이 이미 있어 exactly-once 인프라 비용 없이 안전. 순서는 단일 consumer FIFO + 상태 운반 이벤트로 충분.
- OQ-4: 가용성 degrade 정책 — RabbitMQ 불가 시 행동 규약.
- 상태: Answered (2026-06-05)
- 결정:
1. **버퍼링 금지** — broker 불가 시 intent 를 로컬에 쌓아 복구 후 재생하지 않는다(지나간 결정의 지연 실행 위험 차단). HTTP fallback 은 OQ-1(HTTP 0)으로 선택지에서 제외.
2. **보수적 중단** — broker 불가 시 신규 전략 매매 유입은 중단하고, 전략은 복구 후 신선 데이터로 재판단한다. 스냅샷 RPC 타임아웃 시 전략은 매매를 보류한다.
3. **core 자율 안전 매도는 transport·전략과 독립으로 계속** — trex-core 의 기본 안전 매도(손절 stop-loss, 시간 만기 forced_close)는 RabbitMQ 가용성과 무관하게, 그리고 **전략이 특정 슬롯을 더 이상 패치/관리하지 않아도(dead-man's-switch)** 동작해야 한다. broker 장애나 전략 방치 시에도 열린 포지션은 core 가 보호한다.
- 참고(현행 코드, 정직 기록): 이 안전 매도 기능은 **아직 미구현**이다 — `forced_close_at` 은 필드만 존재(BAT-014/REQ-066 lineage)하고 자동 트리거 로직이 execution 루프에 없으며, stop-loss(손절)는 코드에 전혀 없다. **나중에 구현 예정**이며 본 문서에는 degrade 정책의 전제로 참조만 기입한다(별도 구현 Discovery 는 추후, A-5).
- 반영 위치: `# 5` NFR-3, `# 6` R-3·A-5(신규).
- 종료 조건(충족): degrade 동작·core 안전 매도 독립성 전제가 NFR-3/R-3/A-5 에 반영됨.
- OQ-5: 클라이언트/런타임 통합.
- 상태: Answered (2026-06-05)
- 결정: **`pika`(동기) + 스레드 기반 런타임**. HTTP 제거(OQ-1)로 uvicorn/FastAPI 를 들어내고, 현 sync stack(psycopg 동기·동기 서비스·`threading.Thread` execution/ledger 루프)에 정합하는 순수 sync+thread daemon 으로 재구성한다.
- consumer: intent RPC consumer 스레드(단일, prefetch=1, Postgres commit 후 ack → OQ-3 FIFO/멱등 충족) + snapshot RPC consumer 스레드. pika connection 은 thread-safe 가 아니므로 스레드별 connection/channel 또는 `add_callback_threadsafe` 규약을 따른다.
- 이벤트 publisher: execution 루프 스레드가 체결/종료 시 자기 channel 로 `trex.events` 에 발행(OQ-2/OQ-3).
- entrypoint: `uvicorn.run(app)` → connection 셋업 + consumer/execution/ledger 스레드 기동 + graceful shutdown(기존 `stop` 이벤트 패턴 확장 + connection close).
- 재연결/백오프: pika 는 자동 재연결이 없으므로 직접 구현하며 OQ-4 degrade(보수적 중단) 정책과 묶는다.
- 반영 위치: `# 4` 변경 항목·영향 범위, `# 5` FR-3.
- 종료 조건(충족): 클라이언트·런타임 통합이 FR-3/영향 범위에 반영됨.
- OQ-6: 자격증명/설정 주입 — `.env` AMQP URL 등 접속 정보 주입 패턴.
- 상태: Answered (2026-06-05)
- 결정: 기존 `TREX_*` env 주입 패턴을 재사용한다. AMQP 접속은 단일 URL env **`TREX_RABBITMQ_URL`**(예: `amqp://user:pass@host:5672/vhost`)로 받아 `DaemonConfig.from_env()` 에서 로드한다 — 기존 `TREX_KIS_REDIS_URL`(URL+자격증명을 한 env 로) 패턴과 동형. 자격증명은 URL 에 포함(redis 와 동일 방식).
- 반영 위치: `# 4` 영향 범위(config), `# 5` FR-3, `# 10` DATA.
- 참고: uvicorn 제거(OQ-5)로 기존 `TREX_API_HOST`/`TREX_API_PORT` env 는 폐기 대상이 된다.
- 종료 조건(충족): 주입 방식이 config/영향 범위에 반영됨.
- OQ-7: 운영 probe/admin 표면의 '대체' 범위 — OQ-1 의 HTTP 대체 결정이 전략-facing 통신에 한정인가, `GET /v1/healthz` 같은 앱 레벨 헬스 표면까지 제거하는가? (현 배포 환경: 오케스트레이터 없음 — daemon 은 맨 프로세스(uvicorn)로 실행, repo 에 k8s/compose/Dockerfile 없음. 따라서 '프로세스 살아있음' liveness 는 프로세스 감독(systemd/supervisor/docker restart)이 담당하며 transport 와 무관. HTTP 대체로 잃는 것은 앱 레벨 HTTP 헬스 표면뿐.)
- 상태: Answered (2026-06-05)
- 결정: **(a) 앱 레벨 헬스 표면 제거 — HTTP 표면 0.** `GET /v1/healthz` 포함 모든 HTTP 엔드포인트를 제거한다. liveness("프로세스 살아있음")는 프로세스 감독(systemd/supervisor/docker restart)이 담당하며 transport·broker 와 무관하다(broker 장애가 재시작 루프를 유발하지 않음). 앱 레벨 헬스/관측성은 로그·메트릭으로 대체한다.
- 반영 위치: `# 3` 기대 상태, `# 4` 변경 항목, `# 5` FR-5, `# 6` R-5.
- 영향: 현재 `trex_core/adapters/api/app.py` 의 `healthz` 라우트도 제거 대상에 포함된다.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: stub messaging 어댑터로 스냅샷 request-reply 가 HTTP 와 동일한 logical 응답(필드/마스킹/view 모드)을 반환한다.
- S-2: intent admission RPC 메시지가 슬롯 전이를 일으키고, 중복 전달에도 correlation_id idempotency 로 단일 실행된다.
- S-3: 체결/슬롯 상태 이벤트가 발행되고 구독자가 수신한다.
- S-4: 도메인 import 스윕에서 transport/AMQP 의존이 0 으로 유지된다.
- S-5: interface-contract/runtime-flows/project-structure 가 메시지 계약·MQ 흐름과 정합한다.
- S-6: daemon 기동 후 HTTP 표면이 0 이다(`healthz` 포함 HTTP 포트/엔드포인트 없음, OQ-1/OQ-7).
- S-7: RabbitMQ 불가 시 신규 intent 가 버퍼링되지 않고 보수적으로 중단되며, broker 복구 후에도 옛 intent 가 자동 재생되지 않는다(OQ-4).
- S-8: strategy_id 를 routing key 로 한 이벤트가 해당 전략 queue 에만 전달되고 다른 전략 queue 에는 전달되지 않는다(FR-2/OQ-2).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: stub request-reply 통합테스트 / 데이터 출처: 응답 메시지 본문 vs HTTP 응답.
- S-2 측정 방식: 중복 intent 메시지 주입 테스트 / 데이터 출처: 슬롯 상태·실행 횟수.
- S-3 측정 방식: 이벤트 발행-구독 테스트 / 데이터 출처: 구독자 수신 메시지.
- S-4 측정 방식: 정적 import 스윕 / 데이터 출처: `trex_core/domain` import 목록.
- S-5 측정 방식: 문서-코드 정합 리뷰 / 데이터 출처: baseline 문서 vs 실제 토폴로지/어댑터.
- S-6 측정 방식: 기동 후 포트/라우트 스캔 / 데이터 출처: daemon 런타임(HTTP 리스너 부재).
- S-7 측정 방식: broker 중단 주입 후 intent 발행 테스트 / 데이터 출처: 신규 슬롯 전이 없음·복구 후 재생 없음.
- S-8 측정 방식: 2개 전략 queue 바인딩 후 이벤트 발행 테스트 / 데이터 출처: 각 queue 수신 메시지.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — 특히 logical 계약 불변·전략 로직 제외
- 기준: 운영자가 transport 범위 경계(logical 계약은 dcy-005/002/003 소관)를 확인했다.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-8 측정 방식·출처 명시(본 문서 # 7).
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: HTTP 대체 결정(OQ-1)으로 가용성 결합(R-3)이 High 로 상향(HTTP fallback 부재). delivery(R-2)·가용성(R-3)·big-bang 마이그레이션(R-5) 처리 방침을 운영자가 확인.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-7 이 전부 Answered/Deferred 처리(현재 OQ-1~OQ-7 전부 Answered).
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 운영자가 confirm-discovery 시점에 CONFIRMED_BY 를 지정한다.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-006_20260605_strategy-module-rabbitmq-transport.md
- 참조 문서:
- docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md (스냅샷 read — HTTP 가정 FR-1/A-3, view 모드/strategy_id logical 계약)
- docs/discovery/dcy-002_20260603_slot-core-daemon-http-api.md (HTTP intent API — transport 가정 출처)
- docs/discovery/dcy-003_20260604_broker-execution-and-outbox.md (correlation_id idempotency/outbox — delivery 신뢰성 재사용)
- docs/interface-contract.md, docs/runtime-flows.md, docs/project-structure.md (baseline — 본 Discovery 가 갱신 요구)
- stage-pilot/templates/project-structure.md, stage-pilot/templates/runtime-flows.md (baseline 갱신 논리 템플릿)
- https://github.com/fourblocks2020/trex-core/issues/1 (전략 taxonomy — intent 종류와 연계)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~HTTP 대체 vs 병행~~ → 결정됨: RabbitMQ 가 HTTP `/v1` API 를 **대체**(병행 없음) (OQ-1, 2026-06-05). 운영 probe 잔존 범위는 OQ-7.
- ~~운영 probe/admin 범위~~ → 결정됨: 앱 레벨 헬스 표면 제거, HTTP 표면 0(`healthz` 포함). liveness=프로세스 감독 (OQ-7, 2026-06-05)
- ~~delivery 보증 수준~~ → 결정됨: at-least-once + idempotency = effectively-once. intent=durable commit 후 ack+dedup·단일 consumer FIFO, 이벤트=durable per-strategy queue+상태/시퀀스 운반(순서 불요)+스냅샷 reconcile, exactly-once 미추구 (OQ-3, 2026-06-05)
- ~~클라이언트/런타임 통합~~ → 결정됨: `pika`(동기) + 스레드 런타임, uvicorn 제거, consumer 스레드(intent prefetch=1 FIFO·snapshot), 재연결 직접 구현 (OQ-5, 2026-06-05)
## CONFIRM
- ~~transport 범위 경계~~ → 확정됨(2026-06-05, Justin): In Scope(RabbitMQ 전환·HTTP 완전 제거·delivery 신뢰성·baseline 갱신) / Out of Scope(logical 계약 변경·전략 로직·core 안전 매도 구현·RabbitMQ HA 운영) 확인
- ~~degrade 정책~~ → 결정됨: 버퍼링 금지 + 신규 매매 보수적 중단 + core 자율 안전 매도(손절·시간 만기)는 MQ·전략 독립 동작(미구현, 추후, A-5). dcy-005 OQ-5 와 공통 보수적 안전 (OQ-4, 2026-06-05)
- ~~통신 패턴별 토폴로지~~ → 결정됨: 스냅샷·intent admission = request-reply(direct reply-to), 이벤트 = topic exchange `trex.events`(rk `{event_type}.{strategy_id}.{symbol}`), 요청은 payload·이벤트는 routing key 매핑, intent fill 은 이벤트로 (OQ-2, 2026-06-05)
## DATA
- DATA(비차단, REQ/delivery 단계 계측·provision): RabbitMQ 브로커 인프라 provision + 접속 자격증명 — 주입 방식은 `TREX_RABBITMQ_URL` env 로 확정(OQ-6). 실제 broker 인스턴스/계정 제공은 delivery 단계 운영 작업이며 FR/NFR·범위·성공 기준·검증 시나리오가 충분해 REQ 초안 작성을 막지 않는다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: **OQ-1~OQ-7 전부 Answered** — HTTP 대체(표면 0)·통신 토폴로지(RPC 스냅샷/intent + topic 이벤트)·delivery(effectively-once)·degrade(버퍼링 금지·보수적 중단·core 자율 안전 매도 독립)·클라이언트(pika 동기 + 스레드, uvicorn 제거)·자격증명(`TREX_RABBITMQ_URL`)까지 transport 골격 확정. 남은 인계 전 항목은 transport 범위 경계 확인(운영자)과 CONFIRMED_BY 지정뿐이다. DATA(RabbitMQ 인프라 provision)는 delivery 단계 운영 작업으로 비차단. confirm-discovery 에서 확정 후 REQ Drafting 으로 인계한다.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-05 22:30
@@ -1,259 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-007_20260606_core-autonomous-safety-sell
- 이슈명: core 자율 안전 매도 — stop-loss·take-profit·시간 만기 자동 트리거
- 작성 시각(KST): 2026-06-06
- 마지막 갱신 시각(KST): 2026-06-07 (draft-req: REQ-034~042 생성)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-034, REQ-035, REQ-036, REQ-037, REQ-038, REQ-039, REQ-040, REQ-041, REQ-042
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 참조 파일: 없음
- 관련 TODO/메모: 없음
- 해석:
- REL-001 은 `forced_close_at` 필드만 존재하고 만기 트리거 로직이 없는 상태로 baseline 을 확정했다. stop-loss 는 필드·코드 모두 없다. dcy-006 A-5 와 BAT-007 R-1(High) 에서 "미구현 상태로 운영 release 전 별도 트래킹 필요"로 명시됐다.
- 이번 Discovery 는 (1) snapshot RPC 기반 per-slot 워치독으로 전략 모듈 활성 여부를 감지하고, (2) 워치독 만료 슬롯에 한해 forced_close_at 만기·stop-loss(손절)·take-profit(익절) 가격 조건을 자율 매도로 처리하며, (3) 장 마감 시각(`TREX_MARKET_CLOSE_LIQUIDATION_TIME`)에 전체 `holding` 슬롯을 청산하는 세 가지 기능의 범위·제약·오픈 질문을 정의한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-001~006 어디에도 stop-loss, forced_close_at 자동 트리거, 워치독, 장 마감 강제 청산을 다루는 Discovery 가 없다.
- dcy-006 A-5 는 "추후 구현" 가정으로 명시되었고, REL-001 excluded 로 확정됐다.
- 기존 코드에 `forced_close_at` 필드는 존재하나 감지·실행 루프가 없어 기능 구현이 신규임이 명확하다(`trex_core/domain/slot/models.py:34`).
# 3. 문제점의 요약
- 현재 상태
- 슬롯 도메인에 `forced_close_at: Optional[datetime]` 필드가 존재하고 PostgreSQL 에 영속되지만, 만기 여부를 주기적으로 검사하거나 자동 매도를 제출하는 로직이 없다(`trex_core/domain/slot/models.py:34`).
- stop-loss(손절 가격 임계값) 를 위한 필드·코드가 전혀 없다.
- 전략 모듈이 활성인지 비활성인지를 데몬이 감지하는 메커니즘이 없다.
- RabbitMQ 장애나 전략 모듈 방치 상황에서 열린 포지션이 자동으로 청산되지 않고 무기한 유지될 수 있다(BAT-007 R-1 High).
- 장 마감 후에도 `holding` 슬롯이 자동 청산되지 않는다.
- 기대 상태
- snapshot RPC 응답 시 해당 슬롯들의 워치독 타임스탬프가 갱신되며, `TREX_WATCHDOG_TIMEOUT` 이상 갱신되지 않은 슬롯은 "전략 모듈 미감시" 상태로 판정된다.
- 워치독 만료 슬롯에 한해 `forced_close_at` 만기·stop-loss(손절)·take-profit(익절) 가격 조건 체크를 수행하고, 조건 충족 시 내부 매도 intent 를 제출한다.
- `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST) 이 설정된 경우 해당 시각에 전략 모듈 활성 여부와 무관하게 모든 `holding` 슬롯을 청산한다.
- 자동 매도 intent 는 기존 execution worker 를 통해 실행돼 outbox 내구성·슬롯 상태 전이·ledger 기록을 모두 따른다.
- 이해관계자
- 문서 작성자(AI agent)
- 리뷰어/승인자: Justin Ko
- 운영 담당자: trex-core 데몬 운영자
- 전략 모듈 개발자: snapshot RPC 호출 주기가 워치독 만료 임계값보다 짧아야 한다는 연동 계약 합의 필요
- 제약사항
- trex-core 는 sync+thread 런타임이며, 신규 trigger 루프는 기존 6 스레드 아키텍처를 과도하게 변경하지 않아야 한다.
- `forced_close_at` 필드가 이미 DB 스키마·도메인·persistence 에 존재하므로 하위호환을 유지하면서 트리거만 추가해야 한다.
- trex-core#3(강제 청산/kill-switch) 는 별도 Discovery 로 분리됨(OQ-2 Answered). 이번 Discovery 는 자동 조건 트리거(시간·가격·장 마감)만 다룬다.
- 워치독은 in-memory 이며 데몬 재시작 시 초기화된다. 재시작 후 `TREX_WATCHDOG_GRACE_PERIOD`(기본 60초) 동안 만료 판정 유예(OQ-4 Answered).
## 주요 사용자 시나리오
- 전략 모듈이 N시간 만기 매수 슬롯을 등록(`forced_close_at = 오후 3시`)한 뒤 프로세스를 종료한 경우 — 워치독이 만료되고, 데몬이 3시에 해당 슬롯을 감지해 자동 매도를 제출한다.
- RabbitMQ 장애로 전략 모듈이 snapshot RPC 를 보내지 못하는 상황 — 워치독이 만료되고, stop-loss 조건 충족 시 데몬이 자동 매도를 제출한다.
- 전략 모듈이 정상 동작 중인 슬롯 — 워치독이 주기적으로 갱신되어 만료되지 않으므로 forced_close_at / stop-loss 체크를 건너뛴다(전략 모듈 책임).
- `TREX_MARKET_CLOSE_LIQUIDATION_TIME=15:10` 설정 시 — 오후 3시 10분에 전략 모듈 활성 여부와 무관하게 전체 `holding` 슬롯에 매도 intent 제출.
- 운영자가 CLI 또는 RPC 를 통해 특정 슬롯에 즉시 청산(kill-switch)을 요청하는 경우 — 별도 Discovery(trex-core#3 기반)에서 다룬다. 이번 Discovery 범위 밖.
# 4. 이번에 정의할 변경
- **per-slot 워치독**: snapshot RPC 응답 시 해당 슬롯들의 `last_seen_at` 타임스탬프를 in-memory 로 갱신. `TREX_WATCHDOG_TIMEOUT`(초, env) 경과 시 "미감시" 판정.
- **forced_close_at 만기 감지 루프**: 워치독 만료 슬롯 중 `forced_close_at <= now(KST)` 인 슬롯에 내부 매도 intent 제출. `TREX_FORCED_CLOSE_CHECK_INTERVAL`(초, env) 주기.
- **stop-loss·take-profit 임계값 필드 추가 및 가격 체크**: 슬롯 도메인에 `stop_loss_price: Optional[Decimal]`·`take_profit_price: Optional[Decimal]` 추가. 워치독 만료 슬롯에 한해 KIS `TTTC8397R` polling(`stck_prpr`), 조건 `현재가 <= stop_loss_price OR 현재가 >= take_profit_price` 시 매도 trigger. `TREX_STOP_LOSS_POLL_INTERVAL`(초, env) 주기(두 조건 공통 사용).
- **임계값 갱신 RPC**: `trex.intent.req` 채널에 `action: "update_thresholds"` 추가. `holding` 슬롯의 `stop_loss_price`·`take_profit_price`·`forced_close_at` 를 매수 이후에도 갱신 가능(각 필드는 독립적으로 null 가능). 수신 시 해당 슬롯의 워치독 `last_seen_at` 도 리셋(전략 모듈 활성 증명).
- **장 마감 강제 청산**: `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST, optional) 설정 시 해당 시각에 모든 `holding` 슬롯 매도. 미설정 시 동작 없음. 워치독 만료 여부 무관.
- **자동 매도 내부 경로**: `SlotService.sell()` 직접 호출 — RabbitMQ intent RPC 우회, transport 장애와 독립.
## 영향 범위
- 코드 영향:
- `trex_core/domain/slot/models.py`: `stop_loss_price: Optional[Decimal]`, `take_profit_price: Optional[Decimal]` 필드 추가
- `trex_core/domain/slot/service.py`: forced_close / stop-loss / take-profit 조건 평가 메서드; `update_thresholds(slot_id, stop_loss_price, take_profit_price, forced_close_at)` 메서드
- `trex_core/application/portfolio.py` 또는 신규 모듈: per-slot 워치독 store (`dict[slot_id, datetime]`, in-memory)
- `trex_core/application/messaging_handlers.py`: (1) snapshot RPC handler — 응답 슬롯 `last_seen_at` 갱신; (2) intent RPC — `stop_loss_price`·`take_profit_price` 파싱; (3) `update_thresholds` action handler — `stop_loss_price`·`take_profit_price`·`forced_close_at` DB 갱신 + `last_seen_at` 리셋
- `trex_core/runtime/daemon.py`: safety-sell worker 스레드 추가 — forced-close/stop-loss·take-profit(워치독 만료 슬롯) + 장 마감 청산(전체) 처리
- `trex_core/runtime/config.py`: `TREX_WATCHDOG_TIMEOUT`(기본 120), `TREX_WATCHDOG_GRACE_PERIOD`(기본 60), `TREX_FORCED_CLOSE_CHECK_INTERVAL`(기본 60), `TREX_STOP_LOSS_POLL_INTERVAL`(기본 30), `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(optional) env 파싱
- `trex_core/adapters/persistence/`: `stop_loss_price`, `take_profit_price` 컬럼 추가 DB 마이그레이션
- KIS 현재가 polling 어댑터 신규: `TTTC8397R`(실전) / `VTTC8397R`(모의) 분기, `stck_prpr` 파싱
- `trex_core/application/intents.py`: `IntentIn.stop_loss_price`, `IntentIn.take_profit_price` 필드 추가
- 문서 영향:
- `docs/interface-contract.md`: intent RPC payload 에 `stop_loss_price`·`take_profit_price`·`forced_close_at` 필드, `update_thresholds` action 계약, snapshot RPC 가 워치독 갱신 신호임을 명시
- `docs/runtime-flows.md`: 워치독 갱신 흐름, forced-close checker, stop-loss 가격 체크, 장 마감 청산 흐름 추가
- `docs/project-structure.md`: 신규 모듈/어댑터 반영
- 운영 영향:
- DB 마이그레이션: `stop_loss_price`, `take_profit_price` 컬럼 추가(additive, 기존 슬롯 NULL 허용)
- 신규 데몬 스레드: safety-sell worker (forced-close + stop-loss·take-profit + 장 마감 통합)
- 전략 모듈 연동 계약: snapshot RPC 호출 주기 < `TREX_WATCHDOG_TIMEOUT` 이어야 오탐 없음
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: 데몬은 `TREX_FORCED_CLOSE_CHECK_INTERVAL`(초, 기본 60) 주기로 `holding` 상태이며 **워치독이 만료된** 슬롯 중 `forced_close_at <= now(KST)` 인 슬롯을 감지해 내부 매도 intent 를 제출한다.
- FR-2: `stop_loss_price` 또는 `take_profit_price` 가 설정되고 **워치독이 만료된** 슬롯에 한해, 데몬은 `TREX_STOP_LOSS_POLL_INTERVAL`(초, 기본 30) 주기로 KIS `TTTC8397R`(`stck_prpr`) 를 polling 하고 `현재가 <= stop_loss_price`(손절) 또는 `현재가 >= take_profit_price`(익절) 조건 충족 시 내부 매도 intent 를 제출한다. 두 조건은 동일 polling 루프에서 평가한다.
- FR-3: 자동 매도 intent 는 `SlotService.sell()` 내부 함수 직접 호출로 제출한다(RabbitMQ intent RPC 우회). 기존 outbox 내구성·슬롯 상태 전이·ledger 기록 경로를 따른다.
- FR-4: 이미 `pending_sell` 이거나 자동 매도가 진행 중인 슬롯에 대해 중복 trigger 가 발생하지 않도록 idempotency 를 보장한다.
- FR-5: snapshot RPC handler 는 응답에 포함된 슬롯들의 `last_seen_at` 타임스탬프를 in-memory 워치독 store 에 갱신한다. `now() - last_seen_at > TREX_WATCHDOG_TIMEOUT` 인 슬롯을 "워치독 만료"로 판정한다.
- FR-6: `TREX_MARKET_CLOSE_LIQUIDATION_TIME`(HH:MM KST, optional) 이 설정된 경우 해당 시각에 워치독 만료 여부와 무관하게 모든 `holding` 슬롯에 내부 매도 intent 를 제출한다. 미설정 시 장 마감 청산 동작 없음.
- FR-7: 전략 모듈은 `trex.intent.req` 채널에 `action: "update_thresholds"` 메시지를 발행해 `holding` 상태 슬롯의 `stop_loss_price`·`take_profit_price`·`forced_close_at` 를 갱신할 수 있다(각 필드는 독립적으로 null 가능). 데몬은 DB 를 업데이트하고 해당 슬롯의 워치독 `last_seen_at` 을 현재 시각으로 리셋한다. `holding` 이 아닌 슬롯에 대한 요청은 무시(또는 에러 응답)한다(OQ-6 Deferred → REQ AC).
## 비기능 요구사항
- NFR-1: forced_close_at 만기 감지 지연은 `TREX_FORCED_CLOSE_CHECK_INTERVAL` 이내(기본 60초, env 로 조정 가능). DB/메모리 스캔만 수행하므로 KIS 쿼터 무관.
- NFR-2: 자동 매도 실패(broker 오류) 시 기존 outbox retry 경로를 따르며, 슬롯 상태는 execution worker 결과에 따라 전이된다.
- NFR-3: 가격 조건 polling(`TTTC8397R`) 주기는 `TREX_STOP_LOSS_POLL_INTERVAL`(초, 기본 30) env 로 설정한다(stop-loss·take-profit 공통). 호출 수 상한 = 워치독 만료 슬롯 중 `stop_loss_price` 또는 `take_profit_price` 설정 슬롯 수 × (3600 / `TREX_STOP_LOSS_POLL_INTERVAL`) 로 산정해 KIS 쿼터를 초과하지 않도록 REQ 단계에서 상한을 명시한다.
- NFR-4: 전략 모듈의 snapshot RPC 호출 주기는 `TREX_WATCHDOG_TIMEOUT`(기본 120초) 보다 짧아야 한다. 이 조건이 위반되면 정상 동작 중인 슬롯에서도 워치독 오탐이 발생한다. REQ 에서 권장 관계식(`snapshot_interval < TREX_WATCHDOG_TIMEOUT / 2`, 즉 기본값 기준 60초 이내)을 명시한다.
## 범위 경계
- In Scope
- per-slot 워치독(snapshot RPC 기반 `last_seen_at` 갱신 + 만료 판정)
- `forced_close_at` 만기 자동 매도(워치독 만료 슬롯만)
- `stop_loss_price`·`take_profit_price` 필드 추가 및 가격 기반 자동 매도(워치독 만료 슬롯만, 동일 polling 루프)
- 자동 매도 내부 경로(`SlotService.sell()` 직접 호출)
- 장 마감 강제 청산(`TREX_MARKET_CLOSE_LIQUIDATION_TIME`, 워치독 무관)
- DB 마이그레이션: `stop_loss_price`, `take_profit_price` 컬럼 추가
- intent RPC payload 에 `stop_loss_price`, `take_profit_price` 설정 및 `update_thresholds` 갱신 API 계약 업데이트(`forced_close_at` 포함)
- Out of Scope
- 운영자 수동 즉시 청산(kill-switch): 별도 Discovery(trex-core#3 기반)
- 부분 매도(일부 수량만 청산): REL-001 Feedback Handoff Discovery Input 에 등록된 별도 후보
- trailing stop 등 고급 손절 전략: 단순 임계값 이하 감지만 구현
- 전략 모듈 주도 stop-loss·take-profit 관리: "trex-core 자율" 구현만 대상
# 6. 리스크/가정 목록
## 리스크
- R-1 (Low): stop-loss 가격 소스는 KIS `TTTC8397R` polling 으로 확정(OQ-1 Answered). 워치독 만료 슬롯에만 polling 하므로 전략 모듈 정상 동작 중에는 KIS 쿼터 소비 없음.
- 완화 방안: 워치독 만료 슬롯 수를 기준으로 polling 호출 수를 산정(NFR-3). KIS 쿼터 초과 시 polling 간격을 자동 조정하는 AC 를 REQ 에 추가한다.
- R-2 (Medium): 장 마감 강제 청산(`TREX_MARKET_CLOSE_LIQUIDATION_TIME`) 시 모든 `holding` 슬롯 매도 intent 가 동시에 폭증할 수 있다. execution worker 단일 순차 루프 지연 위험.
- 완화 방안: FR-4(idempotency) 로 중복 방지. execution worker FIFO 큐 처리. 장 마감 전 전략 모듈이 이미 일부 매도했다면 `holding` 슬롯 수가 줄어 폭증 완화. REQ 에서 수용 기준 명시.
- R-3 (Low): trex-core#3 kill-switch 가 별도 Discovery 로 분리됨. FR-3 내부 매도 경로를 kill-switch Discovery 에서 재사용할 수 있도록 별도 함수로 격리한다.
- 완화 방안: FR-3 internal trigger 경로를 별도 함수로 격리해 후속 kill-switch Discovery 에서 재사용 가능하도록 설계한다.
- R-4 (Low): 데몬 재시작 시 워치독 store(in-memory) 가 초기화된다. `TREX_WATCHDOG_GRACE_PERIOD` 동안 만료 판정을 유예해 오탐을 방지한다(OQ-4 Answered). grace period 중에는 forced_close_at / stop-loss 보호 공백이 발생하나, grace period 를 짧게 유지하면 허용 가능한 수준이다.
- 완화 방안: `TREX_WATCHDOG_GRACE_PERIOD` 기본값 60초로 확정됨(A-4). grace period 중에도 FR-6(장 마감 청산)은 동작함을 명시한다.
## 가정
- A-1: 자동 매도 intent 는 `SlotService.sell()` 내부 함수 직접 호출로 제출한다. RabbitMQ intent RPC 를 경유하지 않는다. (**확정** — RabbitMQ 장애와 무관하게 동작해야 하는 "transport 독립" 요건에 부합.)
- A-2: `forced_close_at` 이 설정된 슬롯은 전략 모듈이 의도적으로 만기 청산을 요청한 것이므로, 만기 시점 데몬 자율 매도는 정책적으로 허용된다.
- A-3: stop-loss·take-profit 가격 소스는 KIS API `TTTC8397R`(실전) / `VTTC8397R`(모의) polling 으로 결정됐다(OQ-1 Answered). 평가 기준가는 `stck_prpr`(현재가, 직전 체결가). 트리거 조건: `현재가 <= stop_loss_price`(손절) 또는 `현재가 >= take_profit_price`(익절). 동일 polling 루프에서 평가.
- A-4: 워치독 store 는 in-memory `dict[slot_id, datetime]` 이며 Postgres 에 영속하지 않는다. 데몬 재시작 시 초기화. 재시작 후 `TREX_WATCHDOG_GRACE_PERIOD`(기본 60초, env) 동안 워치독 만료 판정을 유예해 전략 모듈 재연결 시간을 확보한다. 만료 임계값은 `TREX_WATCHDOG_TIMEOUT`(기본 120초, env)(OQ-4 Answered — Grace period).
## 오픈 질문
- OQ-1: stop-loss 를 위한 실시간 가격 소스를 어떻게 조달하는가?
- 상태: Answered
- 결정: KIS API polling(`TTTC8397R` 실전 / `VTTC8397R` 모의), 평가 기준가 `stck_prpr`(현재가). 트리거 조건: `현재가 <= stop_loss_price`(손절) 또는 `현재가 >= take_profit_price`(익절).
- 본문 반영: FR-2, NFR-3, A-3, 코드 영향(KIS 현재가 polling 어댑터).
- OQ-2: trex-core#3(강제 청산/kill-switch) 이 이번 Discovery 에 포함되는가, 별도 Discovery 인가?
- 상태: Answered
- 결정: 별도 Discovery 로 분리. 이번 dcy-007 은 자동 조건 트리거(시간 만기·가격 임계값·장 마감)만 다룬다.
- 본문 반영: Out of Scope(kill-switch 분리 확정), R-3(Low 하향), 제약사항, 사용자 시나리오.
- OQ-3: forced_close_at checker 폴링 간격을 env 로 설정 가능하게 하는가, 고정값으로 하는가?
- 상태: Answered
- 결정: forced_close_at 체크와 stop-loss 가격 polling 을 별도 간격으로 분리하며 둘 다 env 로 설정 가능. `TREX_FORCED_CLOSE_CHECK_INTERVAL`(기본 60초) / `TREX_STOP_LOSS_POLL_INTERVAL`(기본 30초).
- 본문 반영: FR-1, FR-2, NFR-1, NFR-3, 코드 영향(config.py + daemon.py).
- OQ-4: 데몬 재시작 시 워치독이 초기화된 직후 safety-sell 체크를 어떻게 처리하는가?
- 상태: Answered
- 결정: 후보 A — Grace period. 재시작 후 `TREX_WATCHDOG_GRACE_PERIOD`(기본 60초, env) 동안 워치독 만료 판정을 유예. `TREX_WATCHDOG_TIMEOUT` 기본값 120초.
- 본문 반영: A-4(가정 확정 + 기본값), NFR-4(기본값 명시), R-4(Low 하향), 코드 영향(config.py 기본값), DECIDE(OQ-4 완료).
- OQ-5: `update_thresholds` 가 `forced_close_at` 도 갱신 대상에 포함하는가?
- 상태: Answered
- 결정: 포함. `update_thresholds` 는 `stop_loss_price`·`take_profit_price`·`forced_close_at` 세 필드를 모두 갱신 대상으로 한다(각 필드는 독립적으로 null 가능).
- 본문 반영: FR-7(갱신 대상 확장), 코드 영향(`service.py` `update_thresholds()` 시그니처, `messaging_handlers.py` 파싱), S-6(성공 기준 갱신).
- OQ-6: `update_thresholds` 요청이 `holding` 이 아닌 슬롯에 오면 무시(silent)인가, 에러 응답인가?
- 상태: Deferred — REQ AC 단계에서 결정. 에러 응답 시 RPC reply 구조 필요, 무시 시 간단. Freeze 를 막지 않는다.
- 처리 방안: REQ 초안 작성 시 AC 에 명시.
- 종료 조건: REQ 초안의 FR-7 AC 에 반영되면 Answered.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
- S-1: 워치독이 만료된 슬롯 중 `forced_close_at <= now()` 인 슬롯에 자동 매도 intent 가 제출되고 슬롯이 `idle` 로 전이된다.
- S-2: 워치독이 만료된 슬롯에서 `현재가 <= stop_loss_price`(손절) 또는 `현재가 >= take_profit_price`(익절) 조건 충족 시 자동 매도 intent 가 제출되고 슬롯이 `idle` 로 전이된다.
- S-3: 이미 `pending_sell` 인 슬롯에 대해 자동 매도 checker 가 추가 intent 를 제출하지 않는다(idempotency).
- S-4: 워치독이 활성(last_seen_at 갱신 중)인 슬롯은 forced_close_at / stop-loss / take-profit 체크를 건너뛴다.
- S-5: `TREX_MARKET_CLOSE_LIQUIDATION_TIME` 설정 시 해당 시각에 워치독 상태와 무관하게 모든 `holding` 슬롯에 매도 intent 가 제출된다. 미설정 시 동작 없음.
- S-6: `update_thresholds` 메시지 수신 시 `holding` 슬롯의 `stop_loss_price`·`take_profit_price`·`forced_close_at` 가 DB 에 갱신되고, 해당 슬롯의 워치독 `last_seen_at` 이 리셋된다. `holding` 이 아닌 슬롯 요청은 무시된다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: 단위 테스트 — 워치독 만료 + `forced_close_at = now() - 1s` 슬롯에 checker 1회 수행 후 SlotState 전이 확인 / 데이터 출처: pytest 결과
- S-2 측정 방식: 단위 테스트(mock KIS polling) — 워치독 만료 + 손절 조건(임계값 이하) / 익절 조건(임계값 이상) 각각 주입 후 매도 trigger 확인 / 데이터 출처: pytest 결과
- S-3 측정 방식: 단위 테스트 — `pending_sell` 슬롯에 checker 수행 시 추가 intent 미제출 확인 / 데이터 출처: pytest 결과
- S-4 측정 방식: 단위 테스트 — 워치독 활성 슬롯에 checker 수행 시 intent 미제출 확인 / 데이터 출처: pytest 결과
- S-5 측정 방식: 단위 테스트 — 장 마감 시각 mock 주입 후 워치독 활성/만료 슬롯 모두 매도 intent 제출 확인 / 데이터 출처: pytest 결과
- S-6 측정 방식: 단위 테스트 — `update_thresholds` 수신 후 DB 갱신 + `last_seen_at` 리셋 확인; `pending_buy` 슬롯 요청 시 무시 확인 / 데이터 출처: pytest 결과
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-2(kill-switch 포함 여부) 결정 후 범위 경계 확정. → **완료**: kill-switch 별도 Discovery 분리 확정.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: OQ-1(가격 소스) 결정 후 S-2 측정 방식 확정. → **완료**: KIS `TTTC8397R` mock 단위 테스트.
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(High) — OQ-1 Answered 로 해소. 잔여 R-2(Medium)·R-4(Low) → REQ 단계 AC 로 완화.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료. → **완료**: OQ-5 Answered, OQ-6 Deferred. 잔여 Open 항목 없음.
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 가 Freeze 승인.
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md
- 참조 문서:
- docs/discovery/dcy-006_20260605_strategy-module-rabbitmq-transport.md (A-5 가정 참조)
- docs/batches/bat-007_20260605_strategy-module-rabbitmq-transport/index.md (R-1 High 참조)
- docs/releases/rel-001_20260606_trex-core-slot-core-baseline.md (Excluded 확인)
- trex-core#3 (강제 청산/kill-switch GitHub issue — OQ-2 범위 근거)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- ~~OQ-1: Answered — KIS API polling(`TTTC8397R`), 현재가(`stck_prpr`), 트리거 `현재가 <= stop_loss_price`.~~
- ~~OQ-2: Answered — kill-switch 는 별도 Discovery(trex-core#3 기반)로 분리.~~
- ~~OQ-4: Answered — Grace period. `TREX_WATCHDOG_GRACE_PERIOD`(초, env) 동안 만료 판정 유예.~~
- ~~OQ-5: Answered — `update_thresholds` 는 `stop_loss_price`·`take_profit_price`·`forced_close_at` 세 필드를 모두 갱신 대상으로 한다.~~
## CONFIRM
- ~~OQ-3: Answered — `TREX_FORCED_CLOSE_CHECK_INTERVAL`(60초) / `TREX_STOP_LOSS_POLL_INTERVAL`(30초) 별도 env.~~
- ~~A-1: 확정 — `SlotService.sell()` 내부 함수 직접 호출, RabbitMQ intent RPC 우회. transport 독립 요건 부합.~~
## DATA
- trex-core#3 GitHub issue 상세 내용: kill-switch 범위와 구현 방향 확인(OQ-2 resolved, 후속 Discovery 작성 시 참조).
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-5 Answered, OQ-6 Deferred. 모든 Open 항목 해소. FR-7 범위 확정(forced_close_at 포함).
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-07
@@ -1,222 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-008_20260607_emergency-stop-kill-switch
- 이슈명: 비상 정지/강제 청산(kill-switch) — portfolio/전역 슬롯 청산 + 당일 전략 제외
- 작성 시각(KST): 2026-06-07
- 마지막 갱신 시각(KST): 2026-06-07 11:11 (draft-req: REQ-043~047 생성)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-043, REQ-044, REQ-045, REQ-046, REQ-047
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 참조 파일: GitHub #3 (feat: 비상 정지/강제 청산(kill-switch) — portfolio/전역 단위 슬롯 청산 + 당일 전략 제외)
- 관련 TODO/메모: dcy-007 Out of Scope "운영자 수동 즉시 청산(kill-switch): 별도 Discovery(trex-core#3 기반)" 명시
- 해석:
- dcy-007(core 자율 안전 매도)이 자동 조건 기반 청산(워치독 만료 + 가격/시간 트리거)을 구현했지만, 운영자가 즉시 개입할 수 있는 비상 정지 수단은 범위에서 제외됐다. 기존 pause(REQ-032)는 신규 매매만 중단하고 현재 holding 포지션을 청산하지 않으므로 비상 시 실제 보호 수단이 되지 못한다.
- 이번 Discovery는 (1) 운영자가 명령 한 번으로 portfolio/전역 단위 holding 슬롯을 즉시 청산하고, (2) 당일 전략 실행을 중단시키며, (3) 다음 거래일에 자동 복귀하는 비상 정지 기능의 범위·제약·오픈 질문을 정의한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-007이 명시적으로 "운영자 수동 즉시 청산(kill-switch): 별도 Discovery(trex-core#3 기반)"를 Out of Scope로 분리했다.
- 기존 pause(REQ-032)는 holding 슬롯을 청산하지 않으며, day-scoped 제외도 지원하지 않는다. 기능 성격이 근본적으로 다르다.
# 3. 문제점의 요약
- 현재 상태
- trex-core는 `pause/resume` RPC(REQ-032, `trex.pause.req`)로 신규 매매를 중단할 수 있으나, 현재 `holding` 슬롯을 즉시 매도하는 운영자 트리거가 없다(`trex_core/adapters/persistence/postgresql.py`, `PostgreSQLPauseStore`).
- 자율 안전 매도(dcy-007, REL-003)는 워치독 만료 + 가격·시간 조건이 충족된 경우에만 청산하며, 운영자가 즉시 개입하는 비상 경로가 없다.
- kill-switch를 위한 슬롯 청산 경로(`SlotService.sell()`, REQ-036/BAT-010)와 실행 인프라(outbox, execution worker)는 이미 구현되어 있다. 호출 진입점만 없는 상태다.
- 기대 상태
- 운영자가 RPC 메시지 또는 CLI 명령으로 즉시 모든(또는 특정 portfolio의) holding 슬롯을 청산하고 당일 전략 실행을 중단시킬 수 있다.
- 당일 전략 제외는 다음 거래일 자동 복귀한다(day-scoped, pause와 달리 수동 resume 불필요).
- `SlotService.sell()`(REQ-036)을 재사용해 기존 outbox 내구성·슬롯 상태 전이·ledger 기록 경로를 그대로 따른다.
- 이해관계자
- 운영자(Justin Ko): 실 KIS 브로커 연동 환경에서 이상 상황 발생 시 즉시 포지션 청산이 필요한 주요 사용자
- trex-core 데몬 운영자: kill-switch 발동 후 데몬 상태·로그 모니터링 담당
- 전략 모듈 개발자(orb-v1): kill-switch 발동 시 당일 추가 매수 의도가 거부됨을 인지해야 함
- 제약사항
- `SlotService.sell()`(REQ-036)은 HOLDING 슬롯만 대상이며, PENDING_SELL 슬롯에는 idempotent로 동작한다. PENDING_BUY 슬롯 처리는 별도 결정 필요(OQ-3).
- 당일 제외 상태는 데몬 재시작 후에도 유지되어야 하므로 영속화가 필요하다(pause_store 패턴 참조).
- trex-core sync+thread 런타임 — kill-switch 발동 시 다수 슬롯 순차 sell()이 execution worker FIFO 큐에 집중될 수 있다.
## 주요 사용자 시나리오
- 실 KIS 브로커 환경에서 예기치 않은 시장 급변이 발생한 경우 — 운영자가 kill-switch를 발동해 전체 holding 포지션을 즉시 청산하고 당일 추가 매수를 차단한다.
- 특정 portfolio의 전략 오동작을 감지한 경우 — portfolio 단위로 kill-switch를 발동해 해당 portfolio 슬롯만 청산하고, 다른 portfolio는 정상 운행한다.
- 데몬 점검 또는 긴급 배포 전 포지션 청산이 필요한 경우 — 수동 kill-switch로 전체 포지션을 먼저 닫고 데몬을 안전하게 중단한다.
# 4. 이번에 정의할 변경
- **kill-switch 트리거 진입점 추가**: 운영자가 RPC 메시지(`trex.control.req`, `action: "kill_switch"`) 또는 CLI로 kill-switch를 발동하면, 대상 범위(portfolio/전역)의 모든 holding 슬롯에 `SlotService.sell()`을 순차 호출한다. 기존 `trex.pause.req`를 `trex.control.req`로 개념 확장하고 action 필드 기반 dispatch로 구현한다(새 큐 불필요).
- **day-scoped 전략 제외 기능 추가**: kill-switch 발동 후 대상 범위 자산을 당일 전략 실행에서 제외한다. snapshot RPC 응답의 masking 소스에 "kill-switch 당일 제외"를 추가한다. 다음 거래일 데몬 부팅 시 자동 복귀한다.
- **당일 제외 상태 영속화**: kill-switch 당일 제외 상태를 데몬 재시작 후에도 유지하기 위해 DB에 영속한다. **별도 테이블/store 어댑터를 신설**한다(OQ-2 Answered) — `snapshot_pause`(영구 pause)와 분리하고 `trading_date` 키로 day-scoped 표현. 다음 거래일 부팅 시 자동 무효.
- **수동 해제(resume) 지원**: 운영자가 같은 거래일 내에 kill-switch 당일 제외를 해제할 수 있도록 `trex.control.req` 에 `action: "resume_kill_switch"`(범위: portfolio/전역)를 추가한다(OQ-4 Answered). 해당 범위의 day-scoped 제외 row 를 삭제해 정상 매매를 재개한다. 단, 이미 청산된 슬롯은 되돌리지 않는다(매도 비가역).
## 영향 범위
- 코드 영향:
- `trex_core/domain/slot/service.py`: kill-switch 발동 시 복수 슬롯에 sell()을 순차 호출하는 진입점(메서드 또는 헬퍼)
- `trex_core/application/messaging_handlers.py`: `trex.control.req` action dispatch — kill-switch handler 추가
- `trex_core/adapters/persistence/postgresql.py` 또는 신규 어댑터: day-scoped 제외 상태 영속 read/write
- `trex_core/runtime/daemon.py`: pause consumer를 control consumer(`trex.control.req`)로 확장 — action 기반 dispatch
- `trex_core/application/portfolio.py` 또는 snapshot handler: masking 소스에 "kill-switch 당일 제외" 추가
- 문서 영향:
- `docs/interface-contract.md`: kill-switch RPC 메시지 계약(payload 구조, 대상 범위, 응답 구조)
- `docs/runtime-flows.md`: kill-switch 발동 흐름(트리거 → 순차 sell() → day-scoped 제외) 추가
- `docs/project-structure.md`: kill-switch 관련 신규 모듈/handler 반영
- 운영 영향:
- DB 스키마: day-scoped 제외 상태 영속 컬럼 또는 테이블 추가(additive)
- `trex.pause.req` → `trex.control.req` 개념 확장. kill-switch는 `action: "kill_switch"` 필드로 dispatch(새 큐 불필요). daemon.py의 pause consumer 스레드를 control consumer로 교체.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: 운영자가 kill-switch 명령을 발행하면 대상 범위(portfolio_id 지정 또는 전역)의 모든 `holding` 슬롯에 `SlotService.sell(actor="kill-switch", reason="emergency_stop")`을 순차 호출해 즉시 청산을 제출한다.
- FR-2: kill-switch 발동 후 대상 범위의 자산(또는 모든 자산)을 당일 거래일 전략 실행에서 제외한다. snapshot RPC 응답의 masking 소스에 "kill-switch 당일 제외" 항목을 추가한다.
- FR-3: 당일 제외 상태는 데몬 재시작 후에도 유지된다. 다음 거래일 첫 부팅 시 자동으로 해제된다(day-scoped).
- FR-4: portfolio 단위(특정 portfolio_id의 슬롯만)와 전역(모든 슬롯)을 구분해 지원한다.
- FR-5: 이미 `pending_sell` 상태인 슬롯에 대해 sell()을 중복 호출하지 않는다(REQ-036 idempotency 적용).
- FR-6: kill-switch 발동 이력(actor, 발동 시각, 대상 범위, 처리 슬롯 수)이 데몬 로그에 기록된다.
## 비기능 요구사항
- NFR-1: kill-switch 발동 후 holding 슬롯 전체에 대한 sell() 제출 완료까지 지연이 슬롯 수 N 기준 N × 100ms + 5s 이내이어야 한다(순차 처리 기준, 실 KIS 연동 outbox 전송 제외).
- NFR-2: kill-switch 발동이 기존 자율 안전 매도 루프(dcy-007/REL-003)의 동작에 영향을 주지 않는다. 두 기능은 독립적으로 동작한다.
- NFR-3: kill-switch 당일 제외 상태가 영속화되어 데몬 재시작 후에도 snapshot RPC masking이 유지된다.
## 범위 경계
- In Scope
- holding 슬롯 즉시 청산 (portfolio/전역 단위, `SlotService.sell()` 재사용)
- 당일 전략 실행 제외 + 다음 거래일 자동 복귀 (day-scoped, 영속 필요)
- kill-switch 트리거 표면: `trex.control.req` RPC, `action: "kill_switch"` 메시지 (CLI는 REQ 단계에서 결정)
- kill-switch 발동 후 당일 추가 매수 거부 (masking 소스 확장)
- kill-switch 발동 이력 로깅
- Out of Scope
- 부분 수량 청산 (GitHub #5 별도 Discovery)
- 전략 모듈 프로세스 강제 종료 (trex-core 범위 밖)
- pending_buy 슬롯의 롤백 처리 (OQ-3 Deferred — REQ 단계에서 결정)
- kill-switch 발동 후 자율 안전 매도(dcy-007) 비활성화
- trailing stop 등 고급 종료 전략
# 6. 리스크/가정 목록
## 리스크
- R-1 (Medium): kill-switch 발동 시 다수 슬롯에 순차 sell()을 호출하면 execution worker FIFO 큐에 부하가 집중된다. 슬롯 수가 많을 경우 처리 지연이 발생할 수 있다.
- 완화 방안: sell() 호출은 PENDING_SELL 전이만 수행하므로 빠르다(DB write 1회). execution worker는 기존 순차 처리 유지. 슬롯 수 10 미만 운영 환경 기준으로 NFR-1 허용 지연 설정.
- R-2 (해소): 당일 제외 상태 영속 방식 — **별도 테이블/store 신설로 결정**(OQ-2 Answered). pause는 "resume 전까지 유지(persistent)", kill-switch 제외는 "당일만(day-scoped, `trading_date` 키)"으로 테이블 분리 → 의미 충돌 없음, 기존 pause 마스킹 코드 무영향.
- R-3 (Low): kill-switch 발동 중 브로커 오류로 일부 슬롯의 sell()이 실패하거나 지연될 수 있다.
- 완화 방안: sell() → PENDING_SELL 전이 성공 시 기존 outbox retry 경로가 eventual consistency를 보장한다. 발동 이력(FR-6)으로 미처리 슬롯 추적 가능.
- R-4 (Low): kill-switch와 자율 안전 매도(dcy-007)가 동시에 같은 슬롯에 sell()을 호출하면 중복 전이가 발생할 수 있다.
- 완화 방안: REQ-036 idempotency가 PENDING_SELL 상태 중복 sell()을 무시한다. 코드 충돌 없음.
## 가정
- A-1: `SlotService.sell()`(REQ-036/BAT-010)이 이미 구현되어 있어 kill-switch가 직접 재사용할 수 있다.
- A-2: 당일 전략 제외는 snapshot RPC 응답의 masking 소스 확장으로 구현한다. 기존 pause masking 코드 패턴(`portfolio.py`)을 참조한다.
- A-3: kill-switch 발동 후에도 기존 자율 안전 매도 루프(dcy-007)는 독립적으로 동작한다.
- A-4: 운영 환경의 holding 슬롯 수는 통상 10 미만이다. 대규모 병렬 청산 최적화는 Out of Scope.
## 오픈 질문
- OQ-1: kill-switch 트리거 표면 — 기존 `trex.pause.req` 채널에 `action: "kill_switch"` 메시지를 추가할지, 신규 채널(`trex.killswitch.req`)을 별도로 열지?
- 상태: Answered
- 결정: `trex.pause.req` → `trex.control.req` 개념 확장. kill-switch는 `action: "kill_switch"` 필드로 라우팅. 새 큐 불필요. 기능마다 채널을 추가하면 consumer 스레드/커넥션이 기능 수에 비례해 증가하므로 단일 제어 채널 + action discriminator 패턴을 채택한다.
- 반영 위치: # 4 변경 정의, # 4 영향 범위 운영 영향, # 5 범위 경계 In Scope
- OQ-2: 당일 제외 상태 영속 방식 — `PostgreSQLPauseStore`(pause_store)를 확장해 day-scoped 컬럼을 추가할지, 별도 테이블/어댑터를 신설할지?
- 상태: Answered
- 결정: **별도 테이블/어댑터를 신설**한다. kill-switch 당일 제외는 `trading_date` 키로 day-scoped 표현이 자연스럽고, 기존 `snapshot_pause`(portfolio_id PK, resume까지 영구·날짜 없음)와 의미가 달라 같은 테이블에 섞으면 마스킹/만료 로직 복잡도가 올라간다(R-2). 신규 테이블(예: `killswitch_exclusion(portfolio_id, trading_date, actor, ...)`) + 전용 store 어댑터로 관심사를 분리하고, snapshot 마스킹은 `mask = pending | warning | pause | killswitch_today`로 합성한다. 기존 pause 코드는 무영향.
- 반영 위치: # 4 변경 정의(당일 제외 상태 영속화), # 4 영향 범위(신규 테이블/마이그레이션 + store 어댑터), # 6 R-2 완화, # 5 범위 경계
- 종료 조건: 충족(REQ 단계에서 정확한 테이블 컬럼/만료 기준 = trading_date 경과 자동 무효 상세화).
- OQ-3: kill-switch 발동 시 `pending_buy` 상태 슬롯 처리 — 현재 진행 중인 매수를 무시하고 HOLDING이 된 이후에 청산하도록 기다릴지, 즉시 롤백(PENDING_BUY→IDLE) 처리를 별도로 추가할지?
- 상태: Deferred → REQ 단계
- 처리 방안: PENDING_BUY 롤백은 execution 루프와 상호작용이 복잡하므로 REQ 단계에서 상세 설계. 기본 구현은 HOLDING 슬롯만 대상.
- 종료 조건: REQ 초안 작성 시 pending_buy 범위 명시.
- OQ-4: 전역 halt 수동 해제 지원 여부 — kill-switch 발동 후 같은 거래일 내에 운영자가 당일 제외를 해제하고 정상 매매를 재개할 수 있어야 하는가?
- 상태: Answered
- 결정: **수동 해제를 지원한다.** 운영자가 같은 거래일 내에 kill-switch 당일 제외를 해제하고 정상 매매를 재개할 수 있어야 한다. `trex.control.req` 에 `action: "resume_kill_switch"`(범위: portfolio/전역) 를 추가하고, 해당 범위의 day-scoped 제외 row(OQ-2 신규 테이블)를 삭제(또는 무효화)한다. 해제는 "당일 제외"만 풀며 이미 청산된 슬롯을 되돌리지 않는다(매도는 비가역).
- 반영 위치: # 4 변경 정의(수동 해제 action), # 4 영향 범위(control RPC handler), # 5 범위 경계 In Scope, # 10 DECIDE
- 종료 조건: 충족(REQ 단계에서 `resume_kill_switch` payload/응답 + 권한/감사 상세화).
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
# 7. 초기 성공 기준
- S-1: kill-switch 명령 발동 후 모든 holding 슬롯에 PENDING_SELL 전이 제출이 완료된다(슬롯 수 N 기준 NFR-1 이내).
- S-2: kill-switch 발동 후 다음 snapshot RPC 응답에서 대상 자산이 masking되어 buyable/held 뷰에 노출되지 않는다(당일 전략 제외 확인).
- S-3: 데몬 재시작 후에도 kill-switch 당일 제외 상태가 유지되어, 당일 전략 실행이 차단된다.
- S-4: 다음 거래일 데몬 부팅 시 kill-switch 당일 제외 상태가 자동 해제되어 정상 매매 대상이 된다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: kill-switch 발동 후 `slot_repo.list_slots(day)` 결과에서 PENDING_SELL 수 == 발동 전 HOLDING 수 확인 / 데이터 출처: 단위 테스트(mock repo) + 실 e2e 로그
- S-2 측정 방식: kill-switch 발동 후 snapshot RPC 요청 → 응답 assets 목록에서 청산된 symbol 부재 확인 / 데이터 출처: 실 RabbitMQ e2e 테스트 또는 단위 테스트(masking 로직)
- S-3 측정 방식: kill-switch 발동 → 데몬 재시작 → snapshot RPC → masking 지속 확인 / 데이터 출처: 통합 테스트 또는 manual e2e
- S-4 측정 방식: 다음 거래일 날짜로 데몬 부팅 → snapshot RPC → 청산된 symbol이 buyable 뷰에 복귀 확인 / 데이터 출처: 통합 테스트(날짜 mock) 또는 manual e2e
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- OQ-1/2/4 Answered, OQ-3 REQ Deferred. scope = portfolio+전역 둘 다(FR-4). 확정.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- S-1~S-4 각각 단위 테스트/e2e 근거 명시(# 7 측정 방식). 확정.
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- R-1(큐 부하) 완화 = 순차 sell()·N<10 가정으로 NFR-1 인계, R-2(영속 방식) 해소(별도 테이블). REQ 단계 인계.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- OQ-1/2/4 Answered, OQ-3 Deferred → `Open` 없음.
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- Confirmed By = Justin.
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-008_20260607_emergency-stop-kill-switch.md
- 참조 문서:
- GitHub #3: feat: 비상 정지/강제 청산(kill-switch) — portfolio/전역 단위 슬롯 청산 + 당일 전략 제외
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md (Out of Scope 분리 근거)
- docs/srs/Interface/req-036_auto-sell-internal-path.md (SlotService.sell() 재사용)
- docs/srs/Interface/req-032_runtime-pause-resume-control.md (pause 패턴 참조)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- ✅ DECIDE_PERSIST_METHOD (OQ-2, Answered): 당일 제외 상태 영속 방식 = **별도 테이블/store 신설**(`snapshot_pause`와 분리, `trading_date` day-scoped).
- ✅ DECIDE_MANUAL_RESUME (OQ-4, Answered): 수동 해제 **지원** — `trex.control.req action: "resume_kill_switch"` 추가, day-scoped 제외 해제(청산된 슬롯은 비가역).
## CONFIRM
- ✅ CONFIRM_SCOPE_PORTFOLIO_AND_GLOBAL: **portfolio 단위 + 전역 단위 둘 다 포함**. 본문 FR-4("portfolio 단위와 전역 단위 둘 다 구분해 지원") + In Scope("portfolio/전역 단위") + 이슈명에서 이미 확정 — 동일하게 유지.
- ✅ CONFIRM_PENDING_BUY_DEFERRED: **REQ 단계 Deferred로 처리(동의)**. OQ-3(Deferred → REQ 단계) + Out of Scope("pending_buy 슬롯의 롤백 처리 — OQ-3 Deferred")에 비차단으로 명시됨. 기본 구현은 HOLDING 슬롯만 대상.
## DATA
- (비차단, REQ 단계 계측) DATA_SLOT_COUNT_UPPER_BOUND: 실 운영 환경의 최대 동시 holding 슬롯 수. A-4 가정(통상 10 미만)으로 NFR-1(N×100ms+5s) 산정 충분. 실측 상한은 REQ acceptance/verification 단계에서 확정 — Discovery confirm 차단 아님.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1(trex.control.req+action), OQ-2(별도 테이블/store 신설), OQ-4(수동 해제=resume_kill_switch) Answered, OQ-3(pending_buy) REQ Deferred. CONFIRM(scope=portfolio+전역=FR-4, pending_buy=REQ Deferred) 본문 근거로 확정, DATA(slot 수 상한)는 REQ 단계 계측(비차단). 코드 사실관계(SlotService.sell REQ-036, trex.pause.req) 일치, baseline 모순 없음. REQ 초안 작성 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-07 11:11
@@ -1,227 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-009_20260607_multi-instance-rabbitmq-topology
- 이슈명: 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리
- 작성 시각(KST): 2026-06-07
- 마지막 갱신 시각(KST): 2026-06-07 (draft-req: REQ-048~051 생성)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-048, REQ-049, REQ-050, REQ-051
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 참조 파일: trex_core/adapters/messaging/topology.py, trex_core/adapters/messaging/rabbitmq.py, trex_core/runtime/daemon.py, trex_core/runtime/config.py, trex_core/application/watchdog.py
- 관련 TODO/메모: dcy-006(RabbitMQ 도입, confirmed)에서 단일 인스턴스 기준 토폴로지 설계 완료. 다중 인스턴스 시 split-brain 문제와 per-instance 설정 파일 관리는 dcy-006 범위 밖이었음.
- 해석:
- trex-core는 현재 고정 큐명과 단일 `.env` 기반 설정으로 운영되며, 다중 인스턴스를 동일 RabbitMQ에 연결하면 경쟁 소비로 인해 WatchdogStore split-brain·SafetySellService 중복 발동·이벤트 중복 발행 문제가 발생한다.
- 이번 Discovery는 인스턴스별 TOML 설정 파일(`instance_key`, `portfolio_hub_token` 포함)을 도입하고, `instance_key` 기반 큐명 suffix화 + `trex.direct`(topic) + `trex.broadcast`(fanout) exchange 도입으로 인스턴스별 독립 채널을 확보하는 것을 목표로 한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-006(RabbitMQ 도입)은 단일 인스턴스 기준 초기 큐/exchange 설계를 다뤘으며 이미 confirmed + delivered 상태다. 다중 인스턴스 토폴로지와 per-instance TOML 설정 파일은 dcy-006의 연장이 아닌 별도 인프라 변경 주제다.
- 현재 어떤 Discovery/REQ도 `instance_key`, per-instance TOML 설정 파일, Topic exchange 기반 directed routing, Fanout exchange 기반 broadcast를 다루지 않는다.
# 3. 문제점의 요약
- 현재 상태
- `trex_core/adapters/messaging/topology.py`: 모든 큐명이 리터럴 상수(`SNAPSHOT_REQ_QUEUE = "trex.snapshot.req"` 등)로 하드코딩. 인스턴스 suffix 없음.
- `trex_core/adapters/messaging/rabbitmq.py`: `declare_topology()`가 고정 큐명으로 직접 큐를 선언한다. 제어 명령용 directed exchange나 broadcast exchange는 없다.
- `trex_core/runtime/daemon.py`: "single-writer" 설계 명시. 3개 consumer 스레드(intent/snapshot/pause)가 고정 큐명을 구독. 인스턴스 식별자 개념 없음.
- `trex_core/runtime/config.py`: `DaemonConfig.from_env()`가 모든 설정을 환경 변수에서 읽는다. `TREX_PORTFOLIO_HUB_TOKEN`을 포함한 민감값이 `.env`에 위치한다. per-instance 설정 파일 로더 없음.
- `trex_core/application/watchdog.py`: `WatchdogStore._store: dict[str, datetime]` — 프로세스 내 in-memory 저장. 다중 인스턴스 간 공유 불가.
- 문제: 다중 인스턴스를 동일 큐에 연결하면 RabbitMQ가 round-robin으로 메시지를 분배 → snapshot RPC를 받지 못한 인스턴스의 WatchdogStore가 갱신되지 않아 split-brain 발생 → 해당 인스턴스가 모든 슬롯을 watchdog 만료로 판정해 false safety-sell 발동 위험.
- 기대 상태
- 각 인스턴스는 자신의 TOML 설정 파일을 가진다. `instance_key`와 `portfolio_hub_token`은 TOML에 기입하며 `.env`에 넣지 않는 것을 원칙으로 한다.
- `instance_key` 기반 큐명 suffix(`trex.intent.req.{instance_key}`)로 인스턴스별 독립 큐를 선언한다.
- `trex.direct`(topic exchange) 도입: strategy 모듈이 특정 인스턴스에만 메시지를 전달할 때 routing key `{instance_key}.{channel_type}`으로 publish.
- `trex.broadcast`(fanout exchange) 도입: 전역 제어 명령(global kill-switch 등)을 모든 인스턴스의 control 큐에 동시 전달.
- 이해관계자
- 운영자(Justin Ko): 인스턴스별 TOML 설정 파일 관리 및 다중 포트폴리오 분산 운영 담당
- 전략 모듈 개발자(orb-v1): routing key를 `instance_key` 기준으로 publish하도록 인터페이스 계약 변경 인지 필요
- trex-core 데몬 운영자: `--config {path}.toml` CLI 인자로 인스턴스별 설정 파일 지정 + 큐 모니터링
- 제약사항
- pika `BlockingConnection`은 thread-safe하지 않다 — 현재 consumer 스레드별 독립 Connection 패턴은 변경 없이 유지해야 한다.
- 기존 durable 큐(`trex.intent.req` 등)가 이미 RabbitMQ 서버에 선언되어 있을 수 있다 → 새 큐명으로 전환 후 자연 소멸을 허용하며, RabbitMQ admin console에서 적절한 시점에 수동 삭제한다(OQ-1 Answered).
- strategy 모듈(orb-v1)이 routing key 변경에 맞게 publish 코드를 업데이트해야 한다 — 인터페이스 계약 정의가 선행되어야 한다.
- `portfolio_hub_token`과 `instance_key`는 `.env`에 넣지 않는다 — 기존 `config.py`의 `from_env()` 패턴 단독으로는 이 원칙을 지킬 수 없으므로 TOML 로더가 필수다.
## 주요 사용자 시나리오
- orb-v1 전략이 2개의 portfolio를 독립적으로 운영하려는 경우 — 인스턴스A(`instance_key: inst-1`)와 인스턴스B(`instance_key: inst-2`)를 각자의 TOML 파일로 구동해 WatchdogStore/SafetySellService 독립 동작을 보장한다.
- 전체 인스턴스에 global kill-switch를 발동하는 경우 — `trex.broadcast` exchange에 publish하면 모든 인스턴스의 control 큐에 동시 전달되어 인스턴스별 명시 없이 전역 제어가 가능하다.
- 단일 인스턴스 운영을 유지하면서 향후 다중 인스턴스로 전환하는 경우 — TOML 파일에 `instance_key = "default"`로 두고, 추후 인스턴스별 키를 달리해 큐를 분리한다.
# 4. 이번에 정의할 변경
- **per-instance TOML 설정 파일 도입**: 데몬 기동 시 `--config <path>.toml` CLI 인자로 인스턴스별 TOML 파일을 지정한다. 최소 필수 필드: `instance_key`(인스턴스 식별자), `portfolio_hub_token`(portfolio-hub 인증 토큰). 이 두 값은 `.env`에 넣지 않는 것을 원칙으로 한다. `config.py`에 `DaemonInstanceConfig.from_toml(path)` 로더를 추가하고, 기존 `DaemonConfig.from_env()`와 병합해 최종 설정을 구성한다.
- **큐명 동적화 및 `instance_key` 기반 suffix**: `topology.py`의 고정 큐명 상수를 `{base_name}.{instance_key}` 형식의 동적 함수로 교체한다. `daemon.py`가 TOML에서 읽은 `instance_key`를 `declare_topology(instance_key)`에 전달한다.
- **`trex.direct`(topic exchange) 도입 + per-instance 큐 바인딩**: `trex.direct` exchange(type: topic, durable)를 신설하고, 각 인스턴스의 intent/control/snapshot 큐를 routing key `{instance_key}.intent`, `{instance_key}.control`, `{instance_key}.snapshot`으로 바인딩한다.
- **`trex.broadcast`(fanout exchange) 도입 + control 큐 dual binding**: `trex.broadcast` exchange(type: fanout, durable)를 신설하고, 각 인스턴스의 control 큐를 이 exchange에 추가 바인딩한다. global kill-switch 등 전체 인스턴스 대상 제어 명령은 이 exchange에 publish한다.
- **baseline 문서 갱신**: `docs/runtime-flows.md`와 `docs/project-structure.md`에 multi-instance topology 구조와 TOML 기반 기동 흐름을 반영한다.
## 영향 범위
- 코드 영향:
- `trex_core/runtime/config.py`: `DaemonInstanceConfig` dataclass 추가(`instance_key`, `portfolio_hub_token`). `from_toml(path)` 로더 추가. 기존 `DaemonConfig.from_env()`는 유지하되, TOML 로더와 병합하는 `from_config(toml_path)` 팩토리 추가.
- `trex_core/adapters/messaging/topology.py`: 고정 큐명 상수 → 동적 함수(`intent_queue(instance_key)` 등). `DIRECT_EXCHANGE`, `BROADCAST_EXCHANGE` 상수 추가.
- `trex_core/adapters/messaging/rabbitmq.py`: `declare_topology(instance_key)` signature 변경. topic/fanout exchange 선언 + per-instance 큐 바인딩 로직 추가.
- `trex_core/runtime/daemon.py`: `--config` CLI 인자 파싱. TOML에서 `instance_key` 읽기. 큐 선언/구독 시 `instance_key` 전달.
- 진입점(`trex_core/__main__.py` 또는 동등): `--config <path>` CLI 인자 추가.
- 문서 영향:
- `docs/runtime-flows.md`: multi-instance topology 흐름 추가(directed vs broadcast 분기), TOML 기반 기동 절차 반영
- `docs/project-structure.md`: topology.py 역할 변경(동적 큐명 빌더), 설정 파일 계층 반영
- `docs/interface-contract.md`: routing key 계약 갱신 — `{instance_key}.{channel_type}` 규칙 추가
- 운영 영향:
- 인스턴스별 TOML 파일 생성 및 관리 필요(`instance_key`, `portfolio_hub_token` 포함)
- 기존 `trex.intent.req` 등 old 큐는 새 큐 전환 후 자연 소멸을 허용한다. RabbitMQ admin console에서 적절한 시점에 수동 삭제(OQ-1 Answered).
- `TREX_PORTFOLIO_HUB_TOKEN` env var는 TOML로 이관 후 `.env`에서 제거 필요
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: 데몬 기동 시 `--config <path>.toml` CLI 인자는 필수다. 인자 없이 기동하면 오류 메시지를 출력하고 즉시 종료한다. TOML 파일에서 최소 `instance_key`와 `portfolio_hub_token`을 읽는다. 이 두 값은 `.env`에서 읽지 않는다.
- FR-2: `instance_key` 값을 기반으로 모든 수신 큐명을 `{base}.{instance_key}` 형식으로 suffix화한다.
- FR-3: `trex.direct`(type: topic, durable) exchange를 선언하고, 각 인스턴스의 intent/control/snapshot 큐를 routing key `{instance_key}.intent`, `{instance_key}.control`, `{instance_key}.snapshot`으로 바인딩한다.
- FR-4: `trex.broadcast`(type: fanout, durable) exchange를 선언하고, 각 인스턴스의 control 큐를 이 exchange에 추가 바인딩(dual binding)한다. 전체 인스턴스 대상 제어 명령은 이 exchange에 publish한다.
- FR-5: `instance_key = "default"` 단일 인스턴스 운영 시 기존 intent/snapshot/control RPC 경로와 동작이 동일하게 유지된다(하위 호환 검증 기준: 기존 테스트 스위트 회귀 없음).
- FR-6: `docs/runtime-flows.md`와 `docs/project-structure.md`를 multi-instance topology 및 TOML 기반 기동 기준으로 갱신한다.
## 비기능 요구사항
- NFR-1: `instance_key = "default"` TOML로 배포된 환경에서 토폴로지 변경 후 재시작 시 동작 회귀가 없어야 한다(pytest 전체 통과 기준).
- NFR-2: 기존 pika `BlockingConnection` per-thread 패턴을 유지한다. exchange 바인딩 추가로 인해 추가 스레드 또는 커넥션을 생성하지 않는다.
- NFR-3: 인스턴스별 독립 큐 사용 시 WatchdogStore, SafetySellService가 자신의 인스턴스 슬롯만 처리하므로 split-brain이 발생하지 않는다(다중 인스턴스 e2e 시나리오 통과).
- NFR-4: `portfolio_hub_token`과 `instance_key`가 코드/env/로그에 평문으로 노출되지 않는다. TOML 파일은 git에 커밋하지 않는다(`.gitignore` 추가).
## 범위 경계
- In Scope
- per-instance TOML 설정 파일 로더(`instance_key`, `portfolio_hub_token`)
- `instance_key` 기반 큐명 suffix화
- `trex.direct`(topic exchange) 신설 + per-instance 큐 바인딩
- `trex.broadcast`(fanout exchange) 신설 + control 큐 dual binding
- daemon.py/topology.py/rabbitmq.py/config.py/진입점 변경
- 인터페이스 계약(`docs/interface-contract.md`) routing key 규칙 추가
- `docs/runtime-flows.md`, `docs/project-structure.md` 갱신
- `.gitignore`에 `*.toml` 또는 `config/*.toml` 추가(인스턴스 설정 파일 git 제외)
- Out of Scope
- 전체 설정을 TOML로 일원화(KIS 자격증명 등 나머지 env var의 TOML 이관 — 별도 논의)
- strategy 모듈(orb-v1) 코드 변경 — 인터페이스 계약만 정의, 구현은 strategy 모듈 범위
- WatchdogStore Redis 외부화 — competing consumer 방식(별도 Discovery)
- active-passive failover / leader election
- 기존 old 큐 자동 삭제 스크립트 — 운영 절차로 처리
- 인스턴스↔portfolio 매핑 레지스트리 (trex-core 내부 DB 불필요)
# 6. 리스크/가정 목록
## 리스크
- R-1 (Medium): strategy 모듈이 routing key를 변경하지 않으면 다중 인스턴스 환경에서 메시지가 올바른 인스턴스에 도달하지 않는다.
- 완화 방안: 인터페이스 계약에 routing key 규칙을 명시한다. `instance_key = "default"` 단일 인스턴스 시에는 routing key `default.intent` 등으로 publish하면 하위 호환 유지 가능.
- R-2 (Medium): 기존 durable 큐(`trex.intent.req` 등)가 RabbitMQ에 잔류하면 새 큐로 전환 후 old 큐에 consumer가 없어진다.
- 완화 방안: 자연 소멸 허용. 배포 후 old 큐에 새로 publish되는 메시지가 없으므로 메시지 누적 없음. 적절한 시점에 RabbitMQ admin console에서 수동 삭제(OQ-1 Answered).
- R-3 (Low): `trex.broadcast` exchange 메시지가 모든 인스턴스에 동시 전달되므로 인스턴스별로 동일 제어 명령을 중복 처리한다.
- 완화 방안: kill-switch/pause 핸들러에 idempotency 보장(REQ-036 패턴, DB advisory lock). 중복 처리는 무해하게 동작.
- R-4 (Low): TOML 설정 파일이 실수로 git에 커밋되면 `portfolio_hub_token` 등 민감값이 노출된다.
- 완화 방안: `.gitignore`에 인스턴스 TOML 경로 추가(FR-1 범위). TOML 파일은 배포 전용 경로에 위치시킨다.
## 가정
- A-1: 현재 배포 환경은 단일 인스턴스 운영이다. 다중 인스턴스 전환은 향후 필요 시점에 진행한다.
- A-2: strategy 모듈(orb-v1)은 인터페이스 계약 변경에 맞춰 routing key를 업데이트할 수 있다.
- A-3: 인스턴스별 담당 portfolio 매핑은 TOML 설정 또는 strategy 모듈의 routing 설정으로 관리한다. trex-core 내부에 레지스트리 DB를 두지 않는다.
- A-4: `portfolio_hub_token`과 `instance_key`를 `.env`에서 TOML로 이관하면 기존 `.env` 기반 배포 스크립트를 수정해야 한다. KIS 자격증명 등 나머지 env var는 이번 범위에서 이관하지 않는다.
- A-5: TOML 파일 경로는 `--config` CLI 인자로 지정한다. `--config` 인자는 필수이며, 지정하지 않으면 데몬이 오류 메시지를 출력하고 즉시 종료한다(OQ-2 Answered). 기본값 fallback은 제공하지 않는다.
## 오픈 질문
- OQ-1: 기존 durable 큐(`trex.intent.req` 등) 처리 방식 — 자연 소멸(수동 삭제) vs 배포 절차에 명시적 삭제 포함?
- 상태: Answered
- 결정: 자연 소멸 허용. 배포 후 old 큐에 새로 publish되는 메시지는 없으므로 메시지 누적 없음. RabbitMQ admin console에서 적절한 시점에 수동 삭제. 배포 절차에 명시적 삭제 단계를 추가하지 않는다.
- 반영 위치: R-2 완화 방안
- OQ-2: `--config` 인자 없이 데몬 기동 시 동작 — 오류 종료(필수) vs `instance_key = "default"` 기본값으로 기동?
- 상태: Answered
- 결정: `--config` 필수. 인자 없이 기동 시 오류 메시지 출력 후 즉시 종료. 기본값 fallback 없음. 인스턴스 식별이 보장되지 않은 상태로 기동하는 것은 다중 인스턴스 환경에서 split-brain 위험을 내포하므로 명시적 실패가 안전하다.
- 반영 위치: A-5, FR-1
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
# 7. 초기 성공 기준
- S-1: `--config inst-1.toml`(`instance_key = "inst-1"`)으로 인스턴스 기동 시 `trex.intent.req.inst-1`, `trex.snapshot.req.inst-1`, `trex.control.req.inst-1` 큐가 RabbitMQ에 선언되고, `trex.direct` exchange에 올바른 routing key로 바인딩된다.
- S-2: `instance_key = "inst-1"`과 `instance_key = "inst-2"` 인스턴스를 동시 기동했을 때, `inst-1` routing key로 publish된 메시지가 `trex.intent.req.inst-1` 큐에만 전달된다.
- S-3: `trex.broadcast` exchange에 publish된 제어 메시지가 `trex.control.req.inst-1`과 `trex.control.req.inst-2` 큐 모두에 전달된다.
- S-4: `instance_key = "default"` TOML로 기동 시 기존 pytest 스위트(단위 + 회귀) 전체 통과 — 동작 회귀 없음.
- S-5: `portfolio_hub_token`이 `.env`에 없고 TOML에서만 읽혀 `PortfolioHubConfig.token`에 올바르게 주입된다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: RabbitMQ Management API(`GET /api/queues`, `GET /api/bindings`) 또는 pika 큐 선언 성공 로그 / 데이터 출처: 통합 테스트 또는 manual e2e
- S-2 측정 방식: 두 인스턴스 기동 후 `inst-1` routing key로 intent publish → `trex.intent.req.inst-1` 메시지 카운트 +1, `trex.intent.req.inst-2` 카운트 0 확인 / 데이터 출처: RabbitMQ Management API 또는 통합 테스트
- S-3 측정 방식: broadcast exchange에 publish → 두 인스턴스 control 큐 모두 메시지 카운트 +1 / 데이터 출처: 통합 테스트 또는 manual e2e
- S-4 측정 방식: `pytest` 전체 실행 → 0 failures / 데이터 출처: pytest 출력
- S-5 측정 방식: `.env`에 `TREX_PORTFOLIO_HUB_TOKEN` 미설정 상태에서 TOML로 기동 → `PortfolioHubConfig.token` 값 확인 / 데이터 출처: 단위 테스트(config loader)
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1(old 큐 처리) Answered, OQ-2(TOML 필수) Answered
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 각각 테스트 또는 e2e 근거 확정 — S-1~S-5 측정 방식 및 데이터 출처 섹션 7에 전부 기입됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1~R-4 전부 Medium/Low — High 영향 리스크 없음. 모든 리스크에 완화 방안 기입됨. REQ 단계 인계 확정.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1, OQ-2 모두 Answered
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 확인 — Section 11 Confirmed By: Justin Ko
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-009_20260607_multi-instance-rabbitmq-topology.md
- 참조 문서:
- trex_core/adapters/messaging/topology.py (고정 큐명 상수 현황 — 변경 대상)
- trex_core/adapters/messaging/rabbitmq.py (declare_topology 현황 — 변경 대상)
- trex_core/runtime/daemon.py (consumer thread 현황 — instance_key 추가 대상)
- trex_core/runtime/config.py (DaemonConfig.from_env() — TOML 로더 추가 대상)
- docs/discovery/dcy-006_20260605_strategy-module-rabbitmq-transport.md (원래 RabbitMQ 도입 Discovery — 참조 기준)
- docs/runtime-flows.md (baseline 문서 — 갱신 대상, FR-6)
- docs/project-structure.md (baseline 문서 — 갱신 대상, FR-6)
# 10. 사용자 결정 필요 항목 요약
## CONFIRM
- CONFIRM_STRATEGY_MODULE_OUT_OF_SCOPE: 확인됨. strategy 모듈(orb-v1)의 routing key 구현 변경은 이번 Discovery 범위 밖. trex-core는 exchange·바인딩 계약만 정의하며, strategy 모듈의 publish 코드 변경은 별도 Discovery 또는 REQ에서 다룬다(Section 5 Out of Scope 반영).
- CONFIRM_ENV_RESIDUAL: 확인됨. KIS 자격증명(`TREX_KIS_*`), PostgreSQL DSN, TREX_RABBITMQ_URL 등 기존 env var는 이번 범위에서 `.env` 유지. TOML 이관 대상은 `portfolio_hub_token`과 `instance_key`만(Section 5 Out of Scope + A-4 반영).
## DATA
- DATA_TOML_SCHEMA_FULL: REQ 단계 확정. 최소 필드: `instance_key` (string), `portfolio_hub_token` (string). 추가 필드는 REQ 단계 스키마 정의에서 확정한다.
- DATA_MULTI_INSTANCE_TRIGGER_TIMING: REQ/배포 단계 계측 항목. Discovery 단계 범위 밖. 실제 다중 인스턴스 운영 계획은 배포 시점 REQ에서 반영한다.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: TOML 설정 파일 도입 + instance_key 기반 토폴로지 설계 완료. OQ-1(old 큐 자연 소멸), OQ-2(TOML 필수) 모두 Answered. CONFIRM/DATA 항목은 REQ 단계에서 확정 가능 — confirm-discovery 후 REQ 초안 작성 진행 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-07
@@ -1,219 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-010_20260607_safety-sell-persistent-rejection-backoff
- 이슈명: safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책
- 작성 시각(KST): 2026-06-07
- 마지막 갱신 시각(KST): 2026-06-08 (draft-req: REQ-052~054 생성)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-052, REQ-053, REQ-054
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 참조 파일: trex_core/application/safety_sell.py, trex_core/application/execution.py, trex_core/domain/slot/service.py
- 관련 TODO/메모: REL-003 Feedback Handoff — Change Request Input "safety-sell 영속 거부 시 재시도 백오프/쿨다운(신규, 우선순위 ↑)"로 기록됨. 2026-06-07 실 KIS LIVE SL/TP e2e에서 직접 확인된 운영 위험.
- 해석:
- `SafetySellService`가 매도 시도(`HOLDING → PENDING_SELL`)에 성공해도, 브로커 거부로 인한 `PENDING_SELL → HOLDING` 롤백은 별도 `trex-execution` 스레드에서 일어나며 safety-sell이 이를 인지할 수 없다. 결과적으로 트리거 조건이 해소되지 않는 한 동일 슬롯에 매도를 무한 재제출한다.
- 이번 Discovery는 슬롯별 마지막 시도 시각을 `SafetySellService` 인메모리로 추적하는 단순 쿨다운을 도입해, 추가 스레드·DB 스키마 변경 없이 KIS 주문 spam을 방지하는 방법을 정의한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-007(core 자율 안전 매도, confirmed + Implemented)은 watchdog 기반 triggered sell 메커니즘을 정의했으나, 브로커 거부 시 재시도 제한·쿨다운·백오프 정책은 전혀 포함하지 않는다.
- 현재 어떤 Discovery/REQ도 `SafetySellService`의 per-slot 재시도 상태 추적, 최소 재시도 간격, 영속 거부 운영자 경고를 다루지 않는다.
# 3. 문제점의 요약
- 현재 상태
- `trex_core/application/safety_sell.py`: `SafetySellService._safe_sell()`이 `SlotService.sell()` 호출 성공(HOLDING → PENDING_SELL) 시 즉시 `True`를 반환한다. 브로커가 해당 주문을 거부하는지는 알 수 없다.
- `trex_core/application/execution.py`: `_rollback()`이 PENDING_SELL 거부 슬롯을 `PENDING_SELL → HOLDING`으로 전환하고 `clear_dispatch()`한다. 이 사실이 `SafetySellService`에 전달될 피드백 채널이 없다.
- `SafetySellService`에는 per-slot 마지막 시도 시각, 시도 횟수, 쿨다운 만료 시각 등 재시도 상태가 전혀 없다.
- 기본 cadence: stop-loss/take-profit은 30s마다, forced_close는 60s마다 트리거를 재평가한다. 장 휴장(9시간)에서 stop-loss 트리거가 지속되면 최대 9×3600÷30 = 1,080회 KIS 매도 주문이 제출·거부된다.
- paper 브로커는 `reject_symbols` 설정 없이는 주문을 거부하지 않으므로 테스트 환경에서 이 문제가 재현되지 않았다.
- 기대 상태
- `SafetySellService`가 슬롯별 마지막 매도 시도 시각을 인메모리로 추적한다.
- 이전 시도로부터 `min_retry_interval_sec` 이내인 슬롯에는 재시도를 건너뛴다.
- `min_retry_interval_sec` 경과 후 트리거 조건이 여전히 충족되면 자동으로 재시도한다.
- 일정 횟수 이상 재시도가 발생한 슬롯에 대해 운영자 경고 로그를 발행한다.
- DB 스키마 변경 없이 인메모리 상태만으로 구현한다.
- 이해관계자
- 운영자(Justin Ko): KIS 쿼터 보호 및 휴장/규제 상황에서의 안전 운용 책임
- trex-core 데몬 개발자: `SafetySellService` + `SafetySellConfig` 수정 범위 판단
- KIS API: 분당/시간당 쿼터 제한 존재. 영속 거부 시 재제출이 쿼터를 소모해 정상 거래일 운용에 영향 가능.
- 제약사항
- `SafetySellService`와 `ExecutionService`는 별도 스레드이며 공유 데이터 구조에 직접 접근하지 않는다. 피드백 채널 신설은 스레드 간 결합을 증가시키므로 본 Discovery 범위 밖.
- 쿨다운 상태는 데몬 재시작 시 초기화된다. 재시작 직후 1회 재시도는 허용 범위로 본다.
- `SafetySellService`는 단일 스레드에서 호출되므로 쿨다운 `dict` 접근에 별도 락 불필요.
- 브로커 거부 원인 코드(`ack.reason`)를 `SafetySellService`가 직접 수신하는 설계는 이번 Discovery 밖.
## 주요 사용자 시나리오
- (휴장) KIS 장 휴장일에 stop-loss 임계값이 설정된 holding 슬롯이 있을 때 — 쿨다운 없이 30s마다 KIS에 매도 제출·거부가 반복됨. 쿨다운 도입 후: 첫 거부 후 `min_retry_interval_sec` 동안 재시도 없음, 장 재개 후 트리거 조건 재평가.
- (잔고 부족) 계좌 잔고 부족으로 KIS가 매도를 거부하는 경우 — 동일 패턴으로 반복 거부. 쿨다운 중 운영자 경고 로그가 발행되어 수동 조치 가능.
- (규제) 특정 종목 매도 규제가 일시적으로 적용된 경우 — 쿨다운이 규제 해제 시점까지 재시도를 막고, 이후 자동 재평가.
- (정상 거래일) 워치독 만료 슬롯에 stop-loss 트리거 발동 → KIS 매도 접수·체결 → 슬롯 CLOSED → `_holding()`에서 제외. 쿨다운 미발동, 동작 변화 없음.
# 4. 이번에 정의할 변경
- **`SafetySellService` per-slot 쿨다운 추적 도입**: `_last_attempt: dict[UUID, datetime]`와 `_attempt_count: dict[UUID, int]`를 추가한다. `_safe_sell()` 호출 직전 쿨다운 체크를 수행하고, 시도 후 `last_attempt`와 `attempt_count`를 갱신한다. 슬롯이 CLOSED 또는 IDLE로 전환되면(= `_holding()`에서 사라지면) 해당 슬롯의 쿨다운 상태를 자동 정리한다.
- **`SafetySellConfig` 확장**: `min_retry_interval_sec`(기본값 `600`, OQ-1 Answered), `max_retry_warn_count`(기본값 `3`, OQ-2 Answered) 두 env var을 추가한다. 기존 5종 env var(`TREX_WATCHDOG_TIMEOUT` 등)과 같은 방식으로 optional 파싱한다.
- **운영자 경고 로그**: `attempt_count >= max_retry_warn_count`인 슬롯에 대해 `_log.warning("safety-sell repeated rejection: slot=%s count=%d reason=persistent_rejection", ...)` 경고를 발행한다. 이 경고는 쿨다운 체크 통과 후 재시도 직전에 발행한다.
## 영향 범위
- 코드 영향:
- `trex_core/application/safety_sell.py`: `SafetySellService.__init__` 인스턴스 변수 추가(`_last_attempt`, `_attempt_count`). `_safe_sell()` 호출 전후에 쿨다운 check/update 로직 추가. 슬롯 추적 정리 로직 추가.
- `trex_core/runtime/config.py`: `SafetySellConfig`에 `min_retry_interval_sec: int`, `max_retry_warn_count: int` 필드 추가. `from_env()`에서 `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT` 파싱 추가.
- 문서 영향:
- `docs/runtime-flows.md`: safety-sell worker tick 흐름에 "쿨다운 체크 → 건너뜀 or 재시도" 단계 추가
- 운영 영향:
- `TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, `TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT` 두 env var 신설(전부 optional, 기본값 있음). 기존 `.env` 구조 호환.
- 데몬 재시작 시 쿨다운 상태 초기화. 재시작이 의도적이라면 허용 범위.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: `SafetySellService`는 슬롯별 마지막 매도 시도 시각(`last_attempt_at`)과 연속 시도 횟수(`attempt_count`)를 인메모리로 추적한다. 각 트리거(`forced_close`, `stop_loss`, `take_profit`, `market_close`)가 `_safe_sell()`을 호출하기 전에 해당 슬롯의 쿨다운 상태를 확인하고, `now - last_attempt_at < min_retry_interval_sec`이면 해당 슬롯을 건너뛴다.
- FR-2: 쿨다운 만료(`now - last_attempt_at >= min_retry_interval_sec`) 후 트리거 조건이 여전히 충족되면 재시도한다. 재시도 성공 여부와 무관하게 `last_attempt_at`을 갱신한다.
- FR-3: `attempt_count >= max_retry_warn_count`인 슬롯에 대해 재시도 직전에 `WARNING` 로그를 발행한다. 로그에는 `slot_id`, `attempt_count`, `reason`, `last_attempt_at`을 포함한다.
- FR-4: 슬롯이 HOLDING 상태를 벗어나 `_holding()` 목록에서 사라지면(CLOSED, IDLE 등) 해당 슬롯의 `last_attempt_at`과 `attempt_count` 항목을 제거한다.
- FR-5: `SafetySellConfig`에 `min_retry_interval_sec`(`TREX_SAFETY_SELL_MIN_RETRY_INTERVAL`, optional, 기본값 `600`)과 `max_retry_warn_count`(`TREX_SAFETY_SELL_MAX_RETRY_WARN_COUNT`, optional, 기본값 `3`) 두 env var를 추가한다. 미설정 시 기본값으로 동작한다.
- FR-6: `docs/runtime-flows.md`를 갱신해 safety-sell tick 흐름에 "쿨다운 체크" 단계를 반영한다.
## 비기능 요구사항
- NFR-1: 쿨다운 상태는 `SafetySellService` 인메모리 `dict`로만 유지한다. DB 스키마 변경(마이그레이션) 없음.
- NFR-2: `SafetySellService`는 단일 스레드에서 호출되므로 쿨다운 `dict` 접근에 별도 락을 두지 않는다. 다중 스레드 접근이 발생하는 구조 변경은 이번 범위 밖.
- NFR-3: `instance_key = "default"` 환경에서 기존 pytest 스위트(단위 + 회귀) 전체 통과 — 동작 회귀 없음.
- NFR-4: paper 브로커 `reject_symbols` 설정으로 거부를 시뮬레이션했을 때 쿨다운이 발동하고, 쿨다운 만료 후 재시도함을 단위 테스트로 검증한다.
## 범위 경계
- In Scope
- `SafetySellService` 인메모리 per-slot 쿨다운 추적
- `SafetySellConfig` 두 env var 확장(`min_retry_interval_sec`, `max_retry_warn_count`)
- 영속 거부 시 운영자 WARNING 로그
- `docs/runtime-flows.md` safety-sell 흐름 갱신(FR-6)
- Out of Scope
- `ExecutionService` → `SafetySellService` 거부 사유 피드백 채널 신설
- 브로커 거부 원인별(휴장/잔고/규제) 차등 쿨다운 정책
- 쿨다운 상태의 DB 영속화(재시작 후 쿨다운 복원)
- 수동 쿨다운 해제 API(`resume_kill_switch` 유사)
- market_close 트리거에 대한 별도 쿨다운 분리 — market_close는 하루 1회 발동(already guarded by `_last_liquidation_date`)
# 6. 리스크/가정 목록
## 리스크
- R-1 (Medium): 쿨다운 중 트리거 조건이 실제로 해소됐을 때(예: 슬롯 수동 청산, 가격 회복) 불필요한 경고 로그가 계속 발행될 수 있다.
- 완화 방안: 슬롯이 `_holding()`에서 사라지면 쿨다운 상태를 즉시 정리한다(FR-4). 가격 회복으로 트리거 조건 자체가 해소되면 `check_prices()`에서 `_safe_sell()`을 호출하지 않으므로 `attempt_count` 증가도 없다.
- R-2 (Low): 데몬 재시작 시 쿨다운 초기화 → 재시작 직후 영속 거부 슬롯에 1회 즉시 재시도 발생.
- 완화 방안: 재시작은 의도적 행위이므로 1회 재시도는 허용. 재시작 후 재시도가 또 거부되면 쿨다운에 즉시 재진입. 문서에 운영 주의사항으로 명시.
- R-3 (Low): paper 브로커(`reject_symbols` 미설정)에서는 쿨다운이 발동하지 않아 단위 테스트 범위가 제한될 수 있다.
- 완화 방안: paper 브로커 `reject_symbols`에 테스트 대상 종목을 설정해 거부를 시뮬레이션하는 단위 테스트를 추가(NFR-4).
## 가정
- A-1: 영속 거부의 주요 원인(휴장, 잔고 부족, 규제)은 수십 분~수 시간 지속된다. `min_retry_interval_sec = 600s`(기본값)는 일시 거부 원인이 해소되기를 기다리는 중간값으로 적합하다(OQ-1 Answered).
- A-2: 슬롯이 쿨다운 중 HOLDING으로 돌아온 것은 이전 매도 시도가 브로커에서 거부됐음을 의미한다. 별도 피드백 채널 없이도 `_holding()` 재등장으로 암묵적 거부를 감지할 수 있다.
- A-3: market_close 트리거(`check_market_close`)는 하루 1회 `_last_liquidation_date`로 이미 중복 방지가 되어 있어 쿨다운 적용 대상에서 제외한다.
- A-4: `SafetySellService.run_once()`는 `trex-safety-sell` 단일 스레드에서만 호출되므로 쿨다운 `dict` 동시성 문제 없음.
## 오픈 질문
- OQ-1: `min_retry_interval_sec` 기본값 — 어느 정도가 적합한가? (예: 300s / 600s / 1800s)
- 상태: Answered
- 결정: `600s` (10분). 대부분의 일시 거부 원인(잔고 부족, 일시 규제)이 해소되기를 기다리는 중간값. 휴장 9시간 기준 최대 54회 재시도로 쿼터 소모를 수용 가능한 수준으로 제한.
- 반영 위치: FR-5 env var 기본값, A-1
- OQ-2: `max_retry_warn_count` 기본값 및 경고 이후 동작 — 경고 로그만? 아니면 N회 초과 시 쿨다운 무기한 전환 + 수동 해제?
- 상태: Answered
- 결정: `max_retry_warn_count = 3`, 경고 로그만 발행. 무기한 차단 없음. 3회(600s × 3 = 30분 지속 거부) 시점에서 WARNING 로그 발행으로 운영자 인지. 재시도는 쿨다운 만료 후 계속 허용.
- 반영 위치: FR-3, FR-5 env var 기본값
- OQ-3: 트리거 종류별(`forced_close` vs `stop_loss/take_profit`) 쿨다운 분리 여부 — 단일 per-slot 쿨다운으로 통합할지, 트리거별로 독립 쿨다운을 유지할지?
- 상태: Answered
- 결정: 단일 per-slot 쿨다운. 거부 원인(휴장/잔고/규제)은 트리거 종류와 무관하게 슬롯·시장 상태에 종속적이므로 트리거별 분리는 복잡도만 늘린다. `_last_attempt: dict[UUID, datetime]` 단일 구조로 구현.
- 반영 위치: FR-1, Section 4 쿨다운 추적 설계
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
# 7. 초기 성공 기준
- S-1: paper 브로커 `reject_symbols`에 테스트 종목을 설정한 상태에서 stop-loss 트리거 발동 후 `min_retry_interval_sec` 이내에 동일 슬롯 재시도가 없음을 확인한다.
- S-2: 쿨다운 만료 후 트리거 조건이 여전히 충족되면 재시도 1회 실행 및 `last_attempt_at` 갱신 확인.
- S-3: `attempt_count >= max_retry_warn_count` 도달 시 `WARNING` 로그가 발행되고 `slot_id`, `attempt_count`, `reason`이 포함됨을 확인.
- S-4: 슬롯이 CLOSED 전환 후 `_last_attempt`와 `_attempt_count`에서 해당 슬롯 항목이 제거됨을 단위 테스트로 확인.
- S-5: `instance_key = "default"` 환경에서 기존 pytest 스위트 전체 통과 — 동작 회귀 없음.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: `reject_symbols` 설정 + 단위 테스트에서 `run_once()` N회 호출 후 `_safe_sell` 호출 횟수 assert / 데이터 출처: pytest 단위 테스트
- S-2 측정 방식: `last_attempt_at`을 과거 시각으로 설정 후 `run_once()` 호출 → `_safe_sell` 재호출 여부 + `last_attempt_at` 갱신 확인 / 데이터 출처: pytest 단위 테스트
- S-3 측정 방식: `attempt_count`를 `max_retry_warn_count - 1`로 설정 후 `run_once()` → `caplog`에서 WARNING 로그 포함 여부 / 데이터 출처: pytest + `caplog`
- S-4 측정 방식: slot CLOSED 전환 후 `run_once()` → `_last_attempt[slot_id]` KeyError 또는 absent 확인 / 데이터 출처: pytest 단위 테스트
- S-5 측정 방식: `pytest` 전체 실행 → 0 failures / 데이터 출처: pytest 출력
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1(min_retry_interval_sec 기본값), OQ-2(max_retry_warn_count 및 경고 후 동작), OQ-3(쿨다운 분리 여부) 모두 Answered 또는 Deferred
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 각각 pytest 단위 테스트 또는 manual e2e 근거 확정
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1~R-3 완화 방안 REQ 단계 인계 여부 확인 (현재 Medium/Low — High 없음)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1, OQ-2, OQ-3 모두 Answered 또는 Deferred
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 확인
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md
- 참조 문서:
- trex_core/application/safety_sell.py (SafetySellService — 변경 대상)
- trex_core/runtime/config.py (SafetySellConfig — env var 확장 대상)
- trex_core/application/execution.py (_rollback 로직 — 참조 기준, 직접 변경 없음)
- docs/runtime-flows.md (baseline 문서 — safety-sell tick 흐름 갱신 대상, FR-6)
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md (원래 safety-sell Discovery — 참조 기준)
- docs/releases/rel-003_20260607_core-autonomous-safety-sell.md (REL-003 Feedback Handoff — 발견 근거)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- DECIDE_MIN_RETRY_INTERVAL: 확정됨. `min_retry_interval_sec = 600s` (OQ-1 Answered).
- DECIDE_MAX_RETRY_WARN_AND_ACTION: 확정됨. `max_retry_warn_count = 3`, 경고 로그만 발행 (OQ-2 Answered).
- DECIDE_COOLDOWN_GRANULARITY: 확정됨. 단일 per-slot 쿨다운 (OQ-3 Answered).
## CONFIRM
- CONFIRM_MARKET_CLOSE_OUT_OF_SCOPE: 확인됨. `check_market_close`는 `_last_liquidation_date`(safety_sell.py:160)로 하루 1회 이미 보호되어 있어 본 Discovery의 per-slot 쿨다운 적용 대상 밖으로 확정됨(A-3).
## DATA
- DATA_REJECTION_DURATION_STATS: 비차단 운영 데이터. OQ-1이 600s로 확정되어 이 수치 없이도 REQ 초안 작성 가능. 실제 운용 후 수집된 데이터가 있다면 다음 Discovery 또는 change-req 입력으로 반영.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: Safety-sell 쿨다운 메커니즘 설계 완료. OQ-1(600s), OQ-2(3회 경고 로그), OQ-3(단일 per-slot) 모두 Answered. confirm-discovery 전 Justin Ko 최종 승인 필요.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-07
@@ -1,237 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve
- 이슈명: buy intent — portfolio_id를 필수 키로, symbol은 universe(SnapshotStore)에서 resolve (+ portfolio_id 누락 체결의 ledger silent skip 가시화)
- 작성 시각(KST): 2026-06-08 11:08 KST
- 마지막 갱신 시각(KST): 2026-06-08 14:10 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-007(change-req CHG-20260608-01), REQ-018(change-req CHG-20260608-01), REQ-021(change-req CHG-20260608-01), REQ-055(신규) — delivery: BAT-015
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #6, issue #11 (fourblocks2020/trex-core)
- 해석:
- 매수(buy) intent 계약이 `symbol` 필수 / `portfolio_id` optional 로 되어 있어, 정식 식별자(asset_id=portfolio_id) 기반 원장 귀속·safety 보호 범위와 정합이 깨지는 문제를 정식 SDLC 주기로 착수한다.
- 이번 Discovery에서는 buy intent payload 계약을 `portfolio_id` 필수 + 서버측 `symbol` resolve 로 전환하는 범위/리스크/하위호환 경계를 판정한다. 더불어 issue #11 에서 관측된 동일 근본 원인(portfolio_id 없는 슬롯 체결이 ledger 에 silent skip 되어 데이터 누락·운영 가시성 부재)을 함께 흡수해, 예방(admission 거부)과 탐지/가시화(skip 영구 기록·경고)를 한 주기로 다룬다. 이미 Implemented·released(REL-003) 된 계약 변경이므로 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-007/021/018) `change-req` 를 우선하고, 기존 REQ 가 다루지 않는 신규 기능(운영 점검 쿼리 등)만 신규 REQ 로 생성한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-010) 중 buy intent 식별자 계약(portfolio_id 필수화 / symbol resolve)을 다룬 문서는 없다. dcy-004(원장 연동)·dcy-005(snapshot read)·dcy-007/dcy-008(safety)은 portfolio_id 를 전제하지만 buy 제출 계약 자체를 재정의하지 않았다.
- 변경이 이미 Implemented·released 된 buy intent 인터페이스 계약(REQ-007 제출, REQ-021/022 메시지 계약, REL-003 동작)에 닿아 핵심 식별자 의미를 바꾸므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/application/intents.py` 의 `BuyIntent` dataclass 는 `symbol: str`(필수, intents.py:50)이고 `portfolio_id: Optional[str] = None`(optional, intents.py:56) 이다. `_admit_buy`(intents.py:141~)는 `find_idle_slot` 으로 idle 슬롯을 자동 배정한 뒤 `symbol`/`portfolio_id` 를 슬롯에 그대로 기록한다 — symbol 은 필수로 들어오고 portfolio_id 는 없을 수 있다.
- `trex_core/application/messaging_handlers.py` buy 핸들러(73~88행)는 `symbol=str(body.get("symbol") or "")`, `portfolio_id=_opt_str(body.get("portfolio_id"))` 로 파싱해 현 계약(symbol 필수 추정, portfolio_id optional)을 그대로 반영한다.
- universe→symbol resolve 의 기반은 이미 존재한다: `trex_core/application/portfolio.py` 의 `SnapshotStore.get(portfolio_id)`(portfolio.py:130)가 `SnapshotAsset{portfolio_id, symbol, ...}` 를 반환한다. 즉 portfolio_id→symbol 조회 경로는 구현돼 있으나 admission 에서 사용하지 않는다.
- `docs/interface-contract.md` 의 buy payload 계약(79~86행)은 `"symbol": str` 을 기본 필드로, `"portfolio_id": str?` 를 optional 로 명시한다. 즉 문서 계약도 현 코드와 일치한다.
- (issue #11) `trex_core/application/execution.py` 의 `_record_ledger`(execution.py:153~164)는 `slot.portfolio_id` 가 falsy 면 `r.ledger_skipped += 1` 후 즉시 반환한다. 슬롯은 holding/closed 로 정상 전이되지만 `ledger_outbox` 에 행이 생기지 않아 portfolio-hub 에 거래가 전혀 기록되지 않는다. 스킵은 `ExecutionResult` 의 메모리 카운터로만 남고 로그·DB 어디에도 영구화되지 않아 운영자가 누락을 인지하기 어렵다(무성 실패). 실관측: 슬롯 `6e75f3ec-...`(symbol 316140)는 buy/sell 모두 체결돼 closed 됐으나 portfolio_id=None 으로 outbox 0건.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. buy admission 은 기존 runtime flow 에 포함돼 있고, 이번 변경은 그 흐름에 symbol resolve 단계를 추가한다.
- 기대 상태
- buy intent 는 `portfolio_id`(=asset_id, 정식 식별자) 를 필수로 요구하고, `symbol` 은 admission 시 서버가 `SnapshotStore` universe 에서 resolve 해 슬롯에 기록한다.
- portfolio_id 가 universe 에 없으면 명확한 reject(`unknown_portfolio`)로 처리되어, 원장 귀속 불가·safety 보호 누락 슬롯이 생기지 않는다.
- (issue #11) 만에 하나 portfolio_id/execution_id 누락으로 ledger skip 이 발생하더라도 무성으로 사라지지 않고 WARN/ERROR 로그·메트릭으로 노출되며, closed 슬롯 vs outbox 대조 점검 쿼리로 운영자가 누락을 탐지·집계할 수 있다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 계약 변경 및 하위호환 영향 승인
- 운영 담당자 — released(REL-003) buy 계약 변경에 따른 전략 모듈/배포 영향 관리
- 전략 모듈 개발자 — buy intent 제출 호출부(payload) 변경 대상
- 제약사항
- 이미 released(REL-003) 된 인터페이스 계약 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 Discovery) 경유가 필요하다.
- 매수 시점 universe(`SnapshotStore`)에 해당 portfolio_id 가 존재한다는 보장이 거래일/refresh 주기에 의존한다 — resolve 실패 처리 정책이 필요하다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략 모듈이 universe snapshot(`PortfolioItem{portfolio_id, symbol, ...}`)에서 매수 대상을 고른 뒤, buy intent 에 `portfolio_id` 만 실어 제출하면 core 가 symbol 을 universe 에서 resolve 해 슬롯·KIS 주문에 사용한다.
- 운영자가 특정 portfolio 를 pause/kill-switch 로 보호한 상태에서, portfolio_id 필수화 덕분에 모든 buy 슬롯이 보호 범위에 정확히 귀속된다.
- portfolio_id 가 당일 universe 에 없을 때(상장폐지·종목 제외 등) core 가 `unknown_portfolio` 로 매수를 거부해, 귀속 불가능한 체결이 원장에 들어가지 않는다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- buy intent payload 계약을 `portfolio_id` 필수로 전환하고, `symbol` 은 클라이언트 payload 에서 제거해 서버가 universe 에서 resolve 하는 값으로만 사용한다(OQ-1·OQ-2 결정 — override/fallback·레거시 호환 없음).
- admission(`_admit_buy`) 시 `SnapshotStore` 에서 `portfolio_id → symbol` 을 resolve 해 슬롯에 기록하고, universe 에 portfolio_id 부재 시 reject(`unknown_portfolio`) 처리한다.
- (issue #11 제안 ②) `_record_ledger` 의 ledger skip 을 무성에서 가시화로 전환한다 — skip 발생 시 WARN/ERROR 로그·메트릭 노출(OQ-4 결정 — outbox 마커 행은 도입하지 않음; defense-in-depth: portfolio_id 필수화 이후에도 잔존할 수 있는 누락을 탐지 가능하게).
- (issue #11 제안 ③) closed 슬롯 vs `ledger_outbox` 대조 운영 점검 쿼리/알람을 추가해 ledger 누락 슬롯을 주기·온디맨드로 탐지한다.
- 인터페이스 계약 문서(`docs/interface-contract.md`)와 buy admission runtime flow 기술을 새 계약에 맞춰 갱신한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/intents.py` — `BuyIntent`(portfolio_id 필수화, symbol 은 클라이언트 입력에서 제거하고 서버 resolve 로 채움), `_admit_buy`(symbol resolve + unknown_portfolio reject)
- `trex_core/application/messaging_handlers.py` — buy 핸들러 payload 파싱/검증(portfolio_id 필수 검사)
- `trex_core/application/portfolio.py` — `SnapshotStore` portfolio_id→symbol 조회 활용(기존 `get` 재사용, 신규 메서드 필요 시 추가)
- (issue #11) `trex_core/application/execution.py` — `_record_ledger` skip 지점 가시화(로그/메트릭). OQ-4 결정으로 `ledger_outbox` 스키마/`LedgerWriter` 변경은 없음(마커 행 미도입)
- (issue #11) 운영 점검 쿼리(FR-5) — `trex_core/adapters/persistence/sql/` 의 slots↔`ledger_outbox` anti-join 점검 SQL 추가(읽기 전용, 스키마 변경 없음)
- 문서 영향:
- `docs/interface-contract.md` — buy payload 계약(portfolio_id 필수, symbol 필드 제거·서버 resolve) 갱신
- `docs/runtime-flows.md` — buy admission 흐름에 symbol resolve 단계 + ledger skip 가시화 반영
- 운영 영향:
- released(REL-003) buy 계약 변경 → 전략 모듈 호출부 마이그레이션 안내 필요
- 거래일 universe refresh 와 매수 시점 정합 운영 점검 필요
- (issue #11) ledger skip 가시화 + 운영 점검 쿼리로 누락을 운영자가 탐지·집계 가능 → 회계 정합(P&L/포지션) 리스크 완화
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: buy intent payload 는 `portfolio_id` 를 필수 키로 요구하고, 누락 시 admission 단계에서 `portfolio_id_required` 로 거부한다. (issue #11 제안 ① — 근본 원인 제거)
- FR-2: admission 시 `SnapshotStore` universe 에서 `portfolio_id → symbol` 을 resolve 해 슬롯(및 후속 KIS 주문)에 사용할 symbol 로 기록한다. buy payload 는 클라이언트 symbol 을 받지 않는다(OQ-1·OQ-2 결정 — 완전 서버 resolve).
- FR-3: universe 에 해당 `portfolio_id` 가 없으면 `unknown_portfolio` 로 매수를 거부한다. (OQ-1 결정: 단일 reject 정책 — 클라이언트 symbol 로의 override fallback 은 두지 않는다.)
- FR-4: (issue #11 제안 ②) `_record_ledger` 의 ledger skip 발생 시 무성으로 반환하지 않고 WARN/ERROR 로그 + 메트릭으로 노출한다(OQ-4 결정 — `ledger_outbox` 마커 행은 도입하지 않고 영구 탐지는 FR-5 점검 쿼리에 위임). portfolio_id 누락·execution_id 누락 두 skip 경로 모두 포함한다.
- FR-5: (issue #11 제안 ③) closed 슬롯 중 대응 `ledger_outbox` 행이 없는 케이스를 anti-join 으로 식별하는 운영 점검 쿼리/알람을 제공한다(slots 테이블이 durable 하므로 outbox 마커 없이도 영구 탐지 성립 — OQ-4 영구성 위임). 주기 실행 또는 온디맨드 점검으로 누락을 운영자가 탐지·집계할 수 있어야 한다.
- FR-6: `docs/interface-contract.md` 와 `docs/runtime-flows.md` 를 새 buy 계약(portfolio_id 필수 + symbol resolve) 및 ledger skip 가시화/운영 점검에 맞춰 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 원장 정합성 — 모든 매수 슬롯이 `portfolio_id`(asset_id)에 귀속되어 outbox correlation_id(`asset_id:order_id:execution_id`) 및 portfolio-hub ledger 기록이 누락 없이 성립해야 한다.
- NFR-2: safety 일관성 — pause(REQ-032)·kill-switch(dcy-008)·포지션 정합(REQ-031)·snapshot masking 의 portfolio_id 기준 보호 범위에서 누락되는 buy 슬롯이 없어야 한다.
- NFR-3: 즉시 cutover 정합 — 하위호환을 두지 않으므로(OQ-2 결정), 기존 symbol 기반 호출부(전략 모듈/테스트)를 모두 식별해 portfolio_id 기반으로 동시 전환하고, 전환 누락 시 명확히 실패(거부)하도록 보장한다.
- NFR-4: (issue #11) 운영 가시성 — ledger skip 같은 회계 누락 이벤트는 메모리 카운터에만 남지 않고 로그/메트릭/DB 중 최소 하나로 영구 관측 가능해야 한다(무성 실패 금지).
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- buy intent 계약의 portfolio_id 필수화 및 admission 시 symbol resolve
- universe 부재 시 reject 처리 및 인터페이스/런타임 문서 갱신
- (issue #11 제안 ②) ledger skip 의 가시화(로그/메트릭) — defense-in-depth
- (issue #11 제안 ③) closed 슬롯 vs ledger_outbox 대조 운영 점검 쿼리/알람 (OQ-5 결정: 포함)
- Out of Scope
- sell intent 계약 변경(이미 portfolio_id/slot 기반으로 동작)
- universe refresh 주기/스케줄 자체의 재설계
- symbol-only 레거시 호출 한시 허용(호환 창구) — OQ-2 결정으로 미지원(즉시 hard cutover)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): released(REL-003) buy 계약을 깨는 변경이라 기존 symbol 기반 전략 모듈/테스트 호출이 즉시 실패할 수 있다.
- 완화 방안: (OQ-2 결정 — 즉시 cutover) 호환 창구는 두지 않으므로, 전략 모듈·테스트 등 모든 호출부를 사전 식별해 동시에 portfolio_id 기반으로 전환하고, change-req 로 REQ-007/021(및 #11 관련 REQ-018) 변경 이력을 남긴다.
- R-2 (Medium): 매수 시점 universe(`SnapshotStore`)에 portfolio_id 가 없으면(refresh 타이밍·종목 제외) 정상 매수가 `unknown_portfolio` 로 거부될 수 있다.
- 완화 방안: resolve 실패 시 `unknown_portfolio` 단일 reject 정책(OQ-1)을 적용하고, 거부 시 진단 가능한 reason/로그를 남긴다. 거래일 universe refresh 와 매수 시점 정합은 운영 점검 대상으로 둔다.
- R-3 (Low, OQ-1·OQ-2 결정으로 해소): symbol override/fallback·레거시 호환을 모두 두지 않으므로, override 값과 resolve 값의 불일치 모호성이 발생하지 않는다.
- 완화 방안: symbol 은 클라이언트 입력 없이 항상 서버가 universe 에서 resolve 한 값만 사용한다.
- R-4 (Medium, issue #11): portfolio_id 필수화 이전(또는 마이그레이션 창구 중)에 발생한 누락 체결은 이미 ledger_outbox 에 행이 없어 탐지/대응이 늦어질 수 있다.
- 완화 방안: skip 가시화(FR-4)와 운영 점검 쿼리(FR-5)로 신규/잔존 누락을 탐지·집계한다. (과거 누락분의 데이터 복구는 #11 의 명시 요구가 아니므로 본 주기 범위 밖이며, 필요 시 별도 운영 이슈로 다룬다.)
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: `SnapshotStore` 가 admission 시점에 당일 universe 를 보유하며 `get(portfolio_id)` 로 symbol 을 신뢰성 있게 조회할 수 있다.
- A-2: 전략 모듈은 이미 universe snapshot 에서 portfolio_id 를 받아 매수 결정을 내리므로, payload 에서 symbol 을 빼고 portfolio_id 를 싣는 변경이 전략 측 로직에 큰 부담이 아니다.
- A-3: 이번 변경은 buy 경로에 한정되며 sell/kill-switch/pause 등 portfolio_id 기반 기능은 그대로 호환된다.
- A-4: (issue #11) `_record_ledger` 의 execution_id 누락 skip 경로도 동일한 가시화 처리 대상이며, ledger skip 가시화는 슬롯 전이 동작 자체를 바꾸지 않고 관측만 추가한다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: universe 에 portfolio_id 가 없을 때 처리 정책 — `reject(unknown_portfolio)` 단일 정책인가, symbol override fallback 을 허용하는가?
- 상태: Answered (2026-06-08, 사용자 결정 — `reject(unknown_portfolio)` 단일 정책)
- 결정: universe 부재 시 항상 `unknown_portfolio` 로 거부한다. 클라이언트가 보낸 symbol 로의 fallback 은 두지 않는다(원장 정합·식별자 유일성 우선).
- 반영 위치: FR-3, R-3(override fallback 경로 제거로 불일치 위험 해소)
- OQ-2: 하위호환 — 기존 symbol 기반 호출(전략 모듈/테스트)을 한시 허용하는 마이그레이션 창구를 둘지, 즉시 hard cutover 할지?
- 상태: Answered (2026-06-08, 사용자 결정 — 하위호환 없음 / 즉시 hard cutover)
- 결정: 호환 창구를 두지 않는다. symbol 은 payload 에서 제거하고, 모든 호출부를 portfolio_id 기반으로 동시 전환한다. 전환 안 된 호출은 `portfolio_id_required` 등으로 명확히 거부된다.
- 반영 위치: FR-1/FR-2, NFR-3, 범위 경계 Out of Scope, R-1
- OQ-3: 거버넌스 경로 — 본 변경을 신규 REQ(이 Discovery 기반)로 추적할지, 기존 REQ-007/021/022 의 change-req 로 처리할지?
- 상태: Answered (2026-06-08, 사용자 결정 — 가능하면 기존 REQ change-req, 필요 시에만 신규 REQ)
- 결정: 기존 Implemented REQ 본문 수정으로 다룰 수 있는 변경은 `change-req` 로 처리한다. change-req 우선 대상 — REQ-007(intent 제출 API: portfolio_id 필수화), REQ-021(메시지 계약 매핑: buy payload 에서 symbol 제거), REQ-018(체결→원장 기록 fill hook: ledger skip 로그/메트릭 가시화). 기존 REQ 가 다루지 않는 신규 기능(closed 슬롯 vs ledger 대조 운영 점검 쿼리/알람, FR-5)은 신규 REQ 로 생성한다. (OQ-4 로 outbox 마커 행을 두지 않으므로 REQ-019 outbox durability 본문 변경은 원칙적으로 불필요 — REQ 정제에서 영향 확인 후 필요 시에만 포함.)
- 반영 위치: 4. 거버넌스 해석, R-1 완화 방안, 8. 확인 체크
- OQ-4: (issue #11) ledger skip 영구 가시화 방식 — WARN/ERROR 로그+메트릭만으로 충분한가, `ledger_outbox` 에 `skipped` 상태 행까지 남겨 사후 대조 가능하게 할 것인가?
- 상태: Answered (2026-06-08, 사용자 결정 — 로그+메트릭만)
- 결정: skip 시 WARN/ERROR 로그 + 메트릭만 노출하고 `ledger_outbox` 마커 행은 도입하지 않는다. 근거 — skip 마커는 정작 outbox 행 식별자(portfolio_id/execution_id)가 비어 있는 기형 행이 되어 전송 큐 의미를 오염시키고 dispatcher 필터링·스키마 마이그레이션을 요구한다. 영구 탐지는 slots↔outbox anti-join(FR-5)으로 충족된다. 또한 OQ-1·OQ-2 로 portfolio_id 누락 skip 주원인이 admission 에서 제거되어 잔여 skip(execution_id None)은 드문 방어 케이스다.
- 반영 위치: FR-4, FR-5
- OQ-5: (issue #11 제안 ③) closed 슬롯 vs ledger_outbox 대조 운영 점검 쿼리/알람을 이번 주기에 포함할지, 후속으로 분리할지?
- 상태: Answered (2026-06-08, 사용자 결정 — 포함)
- 결정: 운영 점검 쿼리/알람을 이번 주기 In Scope 로 포함한다(FR-5 반영).
- 반영 위치: FR-5, 범위 경계 In Scope, R-4 완화 방안
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: portfolio_id 누락 buy intent 가 100% `portfolio_id_required` 로 거부되고, portfolio_id 만으로 제출한 buy 가 symbol resolve 를 거쳐 정상 admit 된다.
- S-2: admit 된 모든 매수 슬롯이 portfolio_id(asset_id)에 귀속되어 outbox correlation_id 와 ledger 기록이 누락 없이 생성된다.
- S-3: (issue #11) ledger skip 이 발생하는 테스트 시나리오에서 무성 반환이 아니라 WARN/ERROR 로그·메트릭이 100% 남는다.
- S-4: (issue #11 제안 ③) 운영 점검 쿼리가 ledger 누락 closed 슬롯을 정확히 식별한다(누락 슬롯 주입 시 검출, 정상 케이스 시 0건).
- S-5: 변경된 buy 계약이 `docs/interface-contract.md` 와 `docs/runtime-flows.md` 에 반영되고, 코드(intents.py / messaging_handlers.py / execution.py)와 문서가 일치한다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 단위/통합 테스트(portfolio_id 누락·정상·unknown_portfolio 케이스) 통과 / 데이터 출처: `_admit_buy`, buy 핸들러 테스트 결과
- S-2 측정 방식: 매수 시나리오 후 슬롯·outbox·ledger 귀속 검증 / 데이터 출처: 슬롯 스냅샷, outbox correlation_id, portfolio-hub ledger 레코드
- S-3 측정 방식: portfolio_id/execution_id 누락 강제 주입 테스트에서 로그·메트릭 발생 확인 / 데이터 출처: `_record_ledger` 테스트, 데몬 로그, 메트릭
- S-4 측정 방식: 누락 슬롯 주입 후 운영 점검 쿼리 실행 결과 대조 / 데이터 출처: 점검 쿼리 결과, 슬롯·`ledger_outbox` 테이블
- S-5 측정 방식: 코드-문서 정합 리뷰(diff 대조) / 데이터 출처: `docs/interface-contract.md`, `docs/runtime-flows.md`, 변경 코드 diff
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 하위호환 정책이 확정됨 (OQ-2 — 즉시 hard cutover, 호환 창구 없음). 호출부 전환 대상의 구체 식별·수정은 REQ/batch 단계 작업으로 넘김(비차단).
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1 의 change-req 대상(REQ-007/021/018, REQ-019 는 조건부) 추적 방식이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계 담당자 할당 문제로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-5 모두 `Answered`, `Open` 상태 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve.md
- 참조 문서:
- GitHub issue #6 (fourblocks2020/trex-core) — buy intent portfolio_id 필수화 제안
- GitHub issue #11 (fourblocks2020/trex-core) — portfolio_id 없는 거래의 ledger silent skip 버그(동일 근본 원인, 가시화 범위 흡수)
- docs/interface-contract.md — 현행 buy payload 계약(symbol 필수 / portfolio_id optional)
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md — asset_id=portfolio_id 원장 귀속 근거
- trex_core/application/execution.py:153-178 — `_record_ledger` ledger skip 지점(issue #11)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — 모든 결정 항목이 OQ-1~OQ-5 확정으로 해소됨)
> 결정 완료(2026-06-08): OQ-1 universe 부재 처리 = `reject(unknown_portfolio)` 단일 정책 / OQ-2 하위호환 = 없음(즉시 hard cutover) / OQ-3 거버넌스 = 기존 REQ change-req 우선·필요 시 신규 REQ / OQ-4 skip 가시화 = 로그+메트릭만(outbox 마커 없음) / OQ-5 운영 점검 쿼리·알람 = `포함`(FR-5). 모두 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(기존 REQ-007/021/018/019 change-req 우선, 신규 기능만 신규 REQ).
## DATA
- 없음(차단 항목 기준) — 매수 시점 universe(SnapshotStore)에 대상 portfolio_id 존재 보장 여부는 운영 런타임 데이터로, REQ/delivery 단계에서 계측·확정한다. 부재 케이스는 FR-3 의 `unknown_portfolio` reject 와 R-2 로 이미 처리되므로 Discovery confirm 을 막지 않는다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-5 모두 Answered (universe 부재=reject 단일 정책 / 하위호환=없음·즉시 cutover / 거버넌스=기존 REQ change-req 우선·필요 시 신규 REQ / skip 가시화=로그+메트릭만 / 운영 점검 쿼리=포함). C 유형 플레이스홀더 0개, 저장소 사실관계 일치 확인. universe portfolio_id 존재 보장(DATA)은 REQ/delivery 단계 계측 항목으로 비차단 분류. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-08 12:23 KST
@@ -1,233 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-012_20260608_sell-order-pricing-entry-price-bug
- 이슈명: 매도 주문이 현재가가 아닌 entry_price(매수가) 지정가로 나가는 버그 — sell dispatch 가격 정책 부재
- 작성 시각(KST): 2026-06-08 12:30 KST
- 마지막 갱신 시각(KST): 2026-06-08 15:40 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-013/007/036/002/006/003/015 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-007=CHG-20260608-02, 그 외=CHG-20260608-01. delivery: BAT-016
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #10 (fourblocks2020/trex-core), 관련 issue #9(pending_sell 갇힘)
- 해석:
- 매도(sell) 주문 집행 경로(`_dispatch`)가 buy/sell 구분 없이 `slot.entry_price`(매수가)를 지정가로 사용해, 매도가 현재 시장가가 아닌 매수가에 걸린 지정가 주문으로 나가는 정합성 버그를 정식 SDLC 주기로 착수한다.
- 이번 Discovery에서는 sell dispatch 의 가격 결정 정책(시장가 vs 현재가 지정가 vs 인텐트 전달 가격)을 판정하고, safety-sell(자율 매도)과 RPC 재량 매도에 어떤 정책을 적용할지 범위를 정한다. 이미 Implemented·released(REL-001/003) 된 집행 경로(dcy-003 broker execution) 변경이고, 매도 지정가 운반을 위해 슬롯 `entry_price` 를 side 무관 `limit_price` 로 내부 rename(Option A) 하므로, 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-013/007/036/002/006/003/015) `change-req` 로 처리하고 신규 REQ 는 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-011) 중 sell dispatch 의 주문 가격 결정 정책을 다룬 문서는 없다. dcy-003(broker execution + outbox)은 `_dispatch`/주문 제출 경로를 도입했지만 buy/sell 가격 분기를 정의하지 않았고, dcy-007(safety-sell)은 손절/익절 트리거 조건과 `get_current_price` 평가를 다루되 집행 시 매도 주문 가격을 재정의하지 않았다. dcy-010(영속 거부 backoff)은 거부 재시도만 다룬다.
- 변경이 이미 Implemented·released 된 집행 경로(`_dispatch`)의 주문 가격/주문유형(지정가 vs 시장가) 의미를 바꾸고, 매도 인텐트 계약(`SellIntent`)에 optional 가격 필드를 추가하므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/application/execution.py` 의 `_dispatch`(execution.py:84~)는 `side = "buy" if PENDING_BUY else "sell"` 로 side 를 구하지만, 바로 다음 `limit = int(slot.entry_price) if slot.entry_price else None`(execution.py:91)에서 side 를 무시하고 buy/sell 공통으로 `entry_price`(매수가)를 지정가로 쓴다. `order_type` 은 `"00"(지정가) if limit is not None else "01"(시장가)`(execution.py:99)로 결정되므로, 매도도 매수가에 걸린 지정가 주문으로 나간다.
- 버그의 발현은 슬롯의 매수 유형에 의존한다. `entry_price` 는 지정가 매수일 때만 채워지고 시장가 매수에서는 NULL 로 남으며(체결가는 `avg_price` 에만 기록, 매수 체결 시 `entry_price` 미갱신), 매수 후에는 `_dispatch` 의 limit 계산이 유일한 행위적 소비처다. 따라서 (a) 지정가 매수로 진입한 슬롯은 매도 시 `entry_price`(매수가)로 지정가 매도가 나가 버그가 발현되고, (b) 시장가 매수로 진입한 슬롯은 `entry_price=NULL → limit=None → order_type="01"` 로 우연히 시장가 매도가 나가 정상 동작한다. 즉 issue #10 은 지정가 매수 슬롯에서 재현된다.
- `trex_core/application/intents.py` 의 `SellIntent`(intents.py:68~)는 `intent_id/slot_id/reason/sell_strategy_id/extra` 만 가지며 가격 필드가 없다. `_admit_sell`(intents.py:193~)은 `HOLDING → PENDING_SELL` 상태 전이만 수행하고 가격 파라미터를 넘기지 않아, 슬롯의 `entry_price` 는 매수 때 값 그대로 유지된다.
- `trex_core/application/broker_port.py` 의 `get_current_price(symbol)`(broker_port.py:94)는 존재하지만 `_dispatch` 에서 호출되지 않는다. 현재 이 메서드는 safety-sell 워커의 손절/익절 평가에서만 쓰인다(`trex_core/application/safety_sell.py:182`).
- `trex_core/domain/slot/models.py` 의 Slot 은 `avg_price`(매수 체결가, models.py:27)·`entry_price`(매수 지정가, models.py:28)만 가지며 매도 시점 시장가를 담을 필드가 없다.
- `docs/interface-contract.md`(92행)의 sell payload 계약은 `{intent_type:"sell", intent_id, slot_id, reason, sell_strategy_id}` 로, 가격 필드가 없다(현 코드와 일치).
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. sell 집행은 기존 runtime flow(주문 dispatch)에 포함돼 있고, 이번 변경은 그 흐름의 매도 가격 결정 단계를 정정한다.
- 기대 상태
- 매도(`pending_sell`) 집행 시 매수 지정가가 아니라 OQ-1 하이브리드 정책에 맞는 가격으로 주문이 나간다 — 인텐트가 가격을 지정하면 그 지정가, 미지정이면 시장가. (현재가 자동 조회 지정가는 미채택.)
- 손절(stop-loss) 상황에서는 가격 미전달로 시장가 매도가 나가 의도대로 체결되어, 미체결 매도가 `pending_sell` 에 갇히는 문제(issue #9)의 직접 원인이 제거된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 매도 가격 정책 및 집행 경로 변경 승인
- 운영 담당자 — released 집행 경로 변경에 따른 실거래 영향 관리
- 전략 모듈 개발자 — 재량 매도(RPC) 인텐트 호출부 변경 가능성
- 제약사항
- 이미 released(REL-001/003) 된 집행 경로 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 REQ) 경유가 필요하다.
- KIS 주문 계약상 `ORD_DVSN`(00 지정가 / 01 시장가)과 `ORD_UNPR` 의미가 정해져 있어, 매도 가격 유무가 곧 주문유형·가격 필드 매핑을 결정한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- safety-sell 워커가 손절 조건(현재가 < stop_loss_price)으로 매도를 트리거했는데, 집행이 매수가 지정가로 나가 체결되지 않고 슬롯이 `pending_sell` 에 갇힌다 → 정책에 맞는 가격으로 즉시 체결되어야 한다.
- 전략 모듈이 RabbitMQ 로 재량 매도(sell intent)를 보내면, 현재 시장가 기반으로 매도가 집행되어 의도한 청산이 이뤄진다.
- 현재가가 매수가보다 높은 상황에서 매도 시, 매수가에 걸린 불리한 지정가가 아니라 시장가(또는 인텐트가 지정한 가격)로 체결되어 손실 없이 청산된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 슬롯의 매수 전용 `entry_price` 를 **side 무관 "현재 pending 주문 지정가" 단일 필드(내부 rename: `limit_price`)** 로 재정의한다. pending 진입 시(매수: buy intent 지정가 / 매도: sell intent optional 가격) set 하고, pending 이탈(→HOLDING, →CLOSED) 시 None 으로 clear 한다(매수 지정가의 매도 누수 = #10 근본 원인을 구조적으로 차단). 감사 이력은 기존처럼 transition_log 가 보존한다.
- `_dispatch` 는 side 분기로 가격을 고르지 않고 `slot.limit_price` 하나만 읽는다 — 값이 있으면 지정가(`order_type="00"`), 없으면 시장가(`order_type="01"`)(OQ-1 하이브리드).
- `SellIntent`/sell payload 에 optional 매도 가격 필드를 추가하고, `_admit_sell`(및 `SlotService.sell` optional 파라미터)이 그 가격을 슬롯 `limit_price` 로 운반한다. safety-sell/kill-switch 는 가격 미전달 → 시장가.
- 인터페이스 계약 문서(`docs/interface-contract.md`)와 sell 집행 runtime flow 기술을 새 정책에 맞춰 갱신한다. (단, buy/sell payload 의 외부 필드명 `entry_price` rename 은 dcy-012 범위 밖 — REQ-007/dcy-011 과 조율; 본 주기는 내부 슬롯 컬럼/도메인 필드 rename 까지.)
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/domain/slot/models.py` — 슬롯 필드 `entry_price` → `limit_price`(side 무관 "현재 pending 주문 지정가") 내부 rename
- `trex_core/domain/slot/service.py` — `transit_slot`/`_apply_transition_fields` 에서 →HOLDING·→CLOSED 시 `limit_price` 를 None 으로 clear(현 →IDLE 리셋 분기 패턴 재사용), `SlotService.sell` 에 optional 매도 지정가 파라미터 추가(safety-sell/kill-switch 미전달 → 시장가), 매수 경로는 buy 지정가를 `limit_price` 로 set
- `trex_core/application/execution.py` — `_dispatch` 가 `slot.limit_price` 단일 필드만 읽어 가격/주문유형 결정(side 분기 제거, 핵심 버그 지점)
- `trex_core/application/intents.py` — `SellIntent` optional 매도 가격 필드, `_admit_sell` 이 가격을 슬롯 `limit_price` 로 운반. `BuyIntent`/`_admit_buy` 의 매수 지정가도 `limit_price` 로 매핑
- `trex_core/application/messaging_handlers.py` — 외부 payload 필드명(`entry_price`)과 내부 `limit_price` 의 경계 매핑(외부 필드명 rename 은 본 주기 밖)
- `trex_core/adapters/persistence/postgresql.py`, `trex_core/adapters/persistence/sql/` — 슬롯 컬럼 `entry_price` → `limit_price` rename 마이그레이션 + 매핑 갱신
- `trex_core/adapters/broker/kis.py` — `submit_order` 의 `ORD_DVSN`/`ORD_UNPR` 매핑은 OrderRequest 변경에 따라 자동 반영(직접 수정 최소)
- 문서 영향:
- `docs/interface-contract.md` — sell payload 매도 가격 필드 추가 및 주문유형 매핑 갱신
- `docs/runtime-flows.md` — sell 집행 흐름의 가격 결정 단계 정정 + limit_price 생애주기(set/clear)
- `docs/data-model.md` — 슬롯 `entry_price` → `limit_price` 컬럼 의미/이름 갱신
- 운영 영향:
- released 집행 경로의 매도 동작 변경 + 슬롯 컬럼 rename 마이그레이션 → 실거래 매도 체결 방식 변화 및 스키마 변경, 배포 전 검증·마이그레이션 절차 필요
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 슬롯의 매수 전용 `entry_price` 를 side 무관 단일 지정가 필드 `limit_price`(내부 rename)로 재정의한다. `limit_price` 는 pending 진입 시 해당 주문(매수/매도)의 지정가로 set 되고, pending 이탈(→HOLDING, →CLOSED) 시 None 으로 clear 되어 매수 지정가가 매도로 누수되지 않는다.
- FR-2: `_dispatch` 는 side 분기 없이 `slot.limit_price` 만 읽어 주문유형/가격을 결정한다 — 값이 있으면 `order_type="00"`(지정가, 그 가격), 없으면 `order_type="01"`(시장가). 매수가(과거 entry_price 의미)를 매도 지정가로 재사용하지 않는다.
- FR-3: `SellIntent` 및 sell payload 계약에 optional 매도 가격 필드를 추가하고, `_admit_sell`(및 `SlotService.sell` optional 파라미터)이 이를 슬롯 `limit_price` 로 전달한다. 매수 경로는 buy 지정가를 동일 `limit_price` 로 set 한다.
- FR-4: safety-sell·kill-switch(자율 매도)는 가격 미전달로 시장가, RPC 재량 매도는 가격 지정 시 지정가로 동작한다 — 단일 메커니즘(가격 유무 분기)을 모든 매도에 일관 적용한다(OQ-2).
- FR-5: `docs/interface-contract.md`·`docs/runtime-flows.md`·`docs/data-model.md` 를 확정된 매도 가격 정책과 `limit_price` 생애주기(set/clear)·컬럼 rename 에 맞춰 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 체결성 — 매도 트리거(특히 손절)는 가격 미전달로 시장가가 되어 의도대로 즉시 체결되고, 미체결로 `pending_sell` 에 갇히는 케이스(issue #9 연계)를 유발하지 않아야 한다.
- NFR-2: 가격 안전성 — 매도가 매수 지정가에 걸려 불리하게 체결되거나 영원히 미체결되는 정합성 오류가 재발하지 않아야 한다. `limit_price` 의 clear-on-exit 로 매수 지정가가 매도 주문에 잔존하지 않아야 한다.
- NFR-3: 계약 호환 — sell payload 의 매도 가격 필드는 optional 이어야 하며, 미지정(기존 호출 형태)은 시장가로 안전하게 동작해 기존 재량 매도 호출을 깨지 않아야 한다. 외부 payload/read 필드명(`entry_price`)은 본 주기에서 rename 하지 않아 기존 계약 호환을 유지한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 슬롯 `entry_price` → `limit_price` 내부 rename(도메인 모델/DB 컬럼/persistence) + pending 이탈 시 clear 생애주기
- `_dispatch` 가 `slot.limit_price` 단일 필드로 가격/주문유형 결정(side 분기 제거)
- `SellIntent`/sell payload optional 가격 필드 + 슬롯 `limit_price` 운반(`_admit_sell`/`SlotService.sell`)
- 인터페이스/런타임/데이터모델 문서 갱신, 슬롯 컬럼 rename 마이그레이션
- Out of Scope
- buy 주문 가격 의미 변경(매수 지정가는 그대로 `limit_price` 로 set — 동작 동일, 저장 위치명만 통일)
- 외부 buy/sell payload·slot read 의 필드명(`entry_price`) rename — REQ-007/dcy-011 과 조율할 별도 계약 변경
- issue #9(KIS 외부 취소 시 pending_sell 갇힘) 자체의 복구 로직 — 본 버그는 #9 의 한 원인을 제거하나, #9 의 별도 복구는 후속 이슈로 분리
- dispatch 시 현재가 자동 조회(`get_current_price`) 기반 지정가 — OQ-1 에서 미채택(급락 손절 시 재차 미체결 위험)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): released 집행 경로의 매도 동작을 바꾸므로, 정책 선택이 잘못되면 실거래 매도가 의도와 다르게 체결될 수 있다.
- 완화 방안: paper-broker/스텁으로 시나리오(손절/익절/현재가>매수가/현재가<매수가) 검증 후 배포하고, change-req 로 REQ-013/007/036/002/006/003/015 변경 이력을 남긴다.
- R-2 (Medium): 재량 매도가 지정가로 들어오면, 시장가가 그 지정가에 닿지 않을 때 미체결로 `pending_sell` 에 머물 수 있다(지정가의 본질적 특성).
- 완화 방안: 지정가 미체결 처리(취소/재시도/타임아웃)는 기존 dispatch 폴/롤백 경로와 issue #9 복구 범위에서 다루고, 본 주기는 가격 결정 분기에 집중한다. 운영자는 재량 매도 지정가의 미체결 리스크를 인지한다.
- R-3 (Low, OQ-2 결정으로 해소): reason 별 코드 차등을 두지 않고 가격 유무로 분기하므로, safety-sell 은 가격 미전달로 항상 시장가가 되어 손절 체결성이 보장되고 재량 매도만 선택적으로 지정가를 쓴다.
- 완화 방안: `SlotService.sell` 의 가격 파라미터 기본값을 None(시장가)으로 두어 자율 매도 경로가 실수로 지정가를 받지 않도록 한다.
- R-4 (Medium): `entry_price → limit_price` 슬롯 컬럼 rename 과 clear-on-exit 도입은 도메인/persistence/마이그레이션을 건드려, 누락 시 매수 지정가 복원이나 영속 매핑이 깨질 수 있다.
- 완화 방안: 컬럼 rename 마이그레이션과 전이별 set/clear(특히 →HOLDING·→CLOSED clear, →PENDING set)를 단위 테스트로 검증하고, 외부 payload/read 필드명은 그대로 유지(경계 매핑)해 계약 호환을 보존한다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-011(BAT-015) 이 Implemented 되어 매수 시 `symbol` 이 항상 서버 resolve 로 슬롯에 채워지므로, 매도 dispatch 의 KIS 주문에 쓸 `slot.symbol` 이 신뢰성 있게 존재한다(가정 충족 확인됨).
- A-2: safety-sell([safety_sell.py:98](trex_core/application/safety_sell.py))·kill-switch([killswitch.py:61](trex_core/application/killswitch.py))는 `SlotService.sell(slot_id, actor, reason)` 로 가격을 전달하지 않으므로, 가격 파라미터를 optional(default None)로 추가해도 자율 매도 경로는 자동으로 시장가가 된다.
- A-3: 매수 주문 가격 의미는 그대로 유지된다 — buy 지정가는 동일 `limit_price` 로 set 되어 매수 동작은 변하지 않고, 슬롯 상태 전이 규칙(허용 전이 집합)도 유지된다. 변경은 가격 필드의 의미 통일(side 무관)과 pending 이탈 시 clear 추가뿐이다.
- A-4: KIS 시장가 매도(`ORD_DVSN="01"`)가 paper 및 실계좌에서 정상 수용된다(DATA 로 확인).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 매도 가격 정책 — (1) 시장가 매도, (2) 현재가 기반 지정가, (3) 인텐트 전달 가격 중 무엇을 채택하는가(단일 또는 조합)?
- 상태: Answered (2026-06-08, 사용자 결정 — 하이브리드: 인텐트가 가격을 지정하면 지정가, 미지정이면 시장가)
- 결정: `SellIntent`/sell payload 에 optional 매도 가격 필드를 둔다. 가격이 있으면 `order_type="00"`(지정가)로 그 가격에, 없으면 `order_type="01"`(시장가)로 매도한다. 매수가(`entry_price`)는 매도 가격으로 절대 사용하지 않는다. dispatch 에서 `get_current_price` 조회는 하지 않는다(옵션 2 미채택).
- 반영 위치: FR-1/FR-2/FR-3, 4. 변경 항목, 영향 범위
- OQ-2: 정책 적용 범위 — safety-sell(손절/익절/긴급)과 RPC 재량 매도에 동일 정책을 적용하는가, reason 별 차등(예: 손절=시장가, 재량=지정가)을 두는가?
- 상태: Answered (2026-06-08, 사용자 결정 — 가격 유무로 분기, reason 별 코드 차등 없음)
- 결정: 단일 메커니즘(가격 유무 분기)을 모든 매도에 적용한다. safety-sell/kill-switch 는 `SlotService.sell(slot_id, actor, reason)` 로 가격을 전달하지 않으므로 자동으로 시장가가 되어 체결성이 보장된다. 재량 매도(RPC)만 가격을 optional 로 실어 지정가 통제를 쓸 수 있다.
- 반영 위치: FR-4, A-2, R-3
- OQ-3: 거버넌스 경로 — 본 변경을 기존 REQ(dcy-003 집행 계열, sell 인텐트 계약 REQ)의 change-req 로 처리할지, 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-013(주문 집행 루프: `_dispatch` 가 `slot.limit_price` 단일 필드로 결정), REQ-007(intent 제출 API: sell payload optional 매도 가격 필드), REQ-036(자동 매도 내부 경로: `SlotService.sell` optional 가격 파라미터), REQ-002(slot 순수 도메인: `entry_price→limit_price` rename + pending 이탈 clear 전이 의미), REQ-006(PostgreSQL 영속화+스키마: 컬럼 rename 마이그레이션), REQ-003(컬럼 계약 가드 테스트: 컬럼명 변경 반영), REQ-015(브로커/집행 baseline 문서 정합). 하이브리드+rename 모두 기존 REQ 범위 안에서 표현 가능하므로 신규 REQ 는 생성하지 않는다. (Option A 의 rename 으로 change-req 대상이 slot 도메인/persistence 까지 넓어짐 — 비용은 거버넌스 footprint 증가, 이득은 매수 지정가의 매도 누수 구조적 차단.)
- 반영 위치: 1. 거버넌스 해석, R-1 완화 방안, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 매도 dispatch 가 매수 지정가를 재사용하지 않음을 코드/테스트로 보장한다 — `limit_price` 가 pending 이탈 시 clear 되어, 가격 미전달 매도는 `order_type="01"`(시장가), 가격 전달 매도는 `order_type="00"`(해당 지정가)로 발행.
- S-2: safety-sell 손절 트리거 시나리오(가격 미전달)에서 매도가 시장가로 정상 체결되어 `pending_sell` 갇힘이 발생하지 않는다.
- S-3: `entry_price → limit_price` 컬럼 rename 마이그레이션 후 기존 슬롯 데이터가 보존되고, `limit_price` 생애주기(→PENDING set / →HOLDING·→CLOSED clear)가 의도대로 동작한다.
- S-4: 변경된 매도 가격 정책·rename 이 `docs/interface-contract.md`·`docs/runtime-flows.md`·`docs/data-model.md` 에 반영되고, 코드(execution.py, intents.py, slot service/models, persistence)와 문서가 일치한다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 단위/통합 테스트(가격 미전달→시장가, 가격 전달→지정가, entry_price 미사용 케이스) 통과 / 데이터 출처: `_dispatch` 테스트, paper-broker 주문 기록(OrderRequest)
- 검증 필수 조건: 버그는 `entry_price` 가 채워진 **지정가 매수 슬롯**의 매도에서만 재현되므로, 검증 시나리오는 반드시 **지정가 매수(entry_price 채워짐)로 진입한 슬롯의 매도** 케이스를 포함해야 한다. 시장가 매수(entry_price=NULL) 슬롯만 테스트하면 수정 전후 모두 시장가 매도가 나가 버그를 검출하지 못한다. 권장 케이스 — (a) 지정가 매수 슬롯 + 가격 미전달 매도 → 시장가(entry_price 미사용 증명), (b) 지정가 매수 슬롯 + 가격 전달 매도 → 그 지정가, (c) 시장가 매수 슬롯 매도 → 시장가(회귀 안전성).
- S-2 측정 방식: safety-sell 손절 시나리오 e2e 검증 후 슬롯 상태가 closed 로 전이 / 데이터 출처: 슬롯 상태 스냅샷, 주문/체결 로그
- S-3 측정 방식: 마이그레이션 실행 후 슬롯 행 보존 확인 + 전이별 set/clear 단위 테스트(→PENDING set, →HOLDING·→CLOSED clear) 통과 / 데이터 출처: 마이그레이션 SQL, `transit_slot` 테스트, persistence 라운드트립
- S-4 측정 방식: 코드-문서 정합 리뷰(diff 대조) / 데이터 출처: `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/data-model.md`, 변경 코드 diff
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 매도 가격 정책(OQ-1 하이브리드)·적용 범위(OQ-2 가격 유무 분기)·거버넌스(OQ-3) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(실거래 매도 동작 변경)·R-4(컬럼 rename/마이그레이션)의 change-req 대상(REQ-013/007/036/002/006/003/015)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-012_20260608_sell-order-pricing-entry-price-bug.md
- 참조 문서:
- GitHub issue #10 (fourblocks2020/trex-core) — 매도가 entry_price 지정가로 나가는 버그
- docs/discovery/dcy-003_20260604_broker-execution-and-outbox.md — `_dispatch`/주문 제출 경로 도입 근거
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md — safety-sell 트리거·`get_current_price` 평가
- docs/discovery/dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve.md — slot.symbol 서버 resolve 보장(가정 A-1)
- trex_core/application/execution.py:84-104 — `_dispatch` 매도 가격 버그 지점(limit 줄 91)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2 확정으로 매도 가격 정책 해소)
> 결정 완료(2026-06-08): OQ-1 매도 가격 정책 = 하이브리드(가격 지정 시 지정가, 미지정 시 시장가) / OQ-2 적용 범위 = 가격 유무 분기 단일 메커니즘(reason 별 차등 없음).
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-013/007/036/002/006/003/015 change-req, 신규 REQ 없음).
## DATA
- (비차단) KIS 시장가 매도(`ORD_DVSN="01"`)의 paper/실계좌 수용 및 체결 특성 — REQ/delivery 단계의 acceptance/verification 에서 계측·확인할 운영 데이터(A-4). Discovery 본문만으로 FR/NFR·범위·성공 기준이 REQ 초안으로 충분하므로 confirm 차단 사유가 아니다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. Option A(슬롯 `entry_price→limit_price` 내부 rename + clear-on-exit) 설계 반영. 오픈 질문 OQ-1~OQ-3 모두 Answered (매도 가격=하이브리드: 가격 지정 시 지정가/미지정 시 시장가 / 가격 유무 분기, reason 차등 없음 / 거버넌스=REQ-013/007/036/002/006/003/015 change-req, 신규 REQ 없음). C 유형 플레이스홀더 0개, 저장소 사실관계 일치(줄번호 정정 포함). DATA(KIS 시장가 매도)는 REQ/delivery 단계 계측 비차단. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-08 14:50 KST
@@ -1,224 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-013_20260608_kis-external-cancel-pending-sell-stuck
- 이슈명: KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구히 갇힘 — cncl_yn 미파싱
- 작성 시각(KST): 2026-06-08 13:29 KST
- 마지막 갱신 시각(KST): 2026-06-08 16:30 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-012/013/018/031/014/015 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-013/018/015=CHG-20260608-02, REQ-012/031/014=CHG-20260608-01. 워치독 임계 env 는 REQ-014 에 흡수. delivery: BAT-017
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #9 (fourblocks2020/trex-core), 관련 issue #10(dcy-012, 매도 가격 버그)
- 해석:
- 실제 KIS `poll_fill` 이 일별주문체결조회의 취소여부(`cncl_yn`)를 파싱하지 않아, 계좌에서 직접(trex-core 외부) 매도 주문을 취소하면 슬롯이 `pending_sell` 에 영구히 갇히고 데몬이 죽은 주문을 매 tick 무한 폴링하는 버그를 정식 SDLC 주기로 착수한다.
- 이번 Discovery에서는 KIS 어댑터의 취소 감지 갭을 메우는 핵심 수정과, pending 주문의 stale 처리(워치독/타임아웃) 보강 범위를 판정한다. 이미 Implemented·released(REL-001/003) 된 집행/브로커 경로(dcy-003) 변경이므로, 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-012/013/018/031/014/015) `change-req` 로 처리하고 신규 REQ 는 원칙적으로 생성하지 않는다(워치독 임계 env 는 조건부).
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-012) 중 KIS 외부 취소 감지(`cncl_yn` 파싱)나 pending 주문 stale 워치독을 다룬 문서는 없다. dcy-003(broker execution+outbox)은 집행 루프·롤백 경로를 도입했고, dcy-012(issue #10)는 매도 *가격* 버그를 다뤘으나 `poll_fill` 의 취소 *감지* 갭은 명시적으로 범위 밖(후속 이슈)으로 분리했다.
- 변경이 이미 Implemented·released 된 KIS 어댑터(`poll_fill`)와 집행 reconciliation 의미를 바꾸므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 취소를 종료 상태로 다루는 설계는 이미 존재한다 — `trex_core/application/broker_port.py` 의 `FillStatus.is_terminal_reject`(broker_port.py:63~64)는 `rejected`·`cancelled` 를 모두 포함하고, `_poll`(execution.py:113~)은 `is_terminal_reject` 면 `_rollback`(execution.py:198~)을 호출해 `PENDING_SELL → HOLDING` 으로 되돌리고 `clear_dispatch` 로 `broker_order_id` 를 비운다(execution.py:207~210).
- 그러나 실제 KIS `poll_fill`(`trex_core/adapters/broker/kis.py:163~208`)은 취소여부(`cncl_yn`)를 파싱하지 않는다. 계좌에서 직접 취소하면 일별주문체결조회(`inquire-daily-ccld`)에 주문 row 는 남지만 체결수량(`tot_ccld_qty`)이 0이므로, `if ccld_qty <= 0: return FillStatus(state="accepted", ...)`(kis.py:191~192)로 빠져 **`accepted`(접수됨)** 로 판정된다. `_poll` 은 `accepted` 를 "다음 tick 대기"로 처리해 슬롯이 영구히 `pending_sell` 에 머문다.
- 부분 체결 후 취소(`0 < ccld_qty < ord_qty`)도 같은 문제다 — `partially_filled`(kis.py:207~)로 판정되어 역시 "다음 tick 대기"로 처리되어 갇힌다.
- paper 브로커는 취소를 올바르게 처리한다 — `trex_core/adapters/broker/paper.py:74~75` 에서 `o.cancelled` 면 `FillStatus(state="cancelled")` 를 반환한다. 즉 갭은 **실제 KIS 어댑터에만** 존재한다.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. 집행 폴/롤백은 기존 runtime flow 에 포함돼 있고, 이번 변경은 KIS 어댑터의 취소 감지 단계를 메운다.
- 기대 상태
- KIS `poll_fill` 이 외부 취소를 `FillStatus(state="cancelled")` 로 매핑하면, 기존 롤백 경로가 동작해 슬롯이 `HOLDING` 으로 복귀하고 `broker_order_id` 가 비워져 자동 회복된다. (dcy-012 구현 가정 — HOLDING 진입 시 `limit_price` 가 clear 되어 재매도는 시장가로 나가 체결성이 보장된다.)
- 죽은 주문에 대한 무한 1초 폴링이 멈추고, 운영자 수동 개입 없이 자동 복구된다. 보강책으로 pending 주문이 비정상적으로 오래 머무는 경우 stale 처리(워치독/타임아웃)가 안전망이 된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — KIS 어댑터·reconciliation 변경 승인
- 운영 담당자 — 외부 취소·갇힘 상황의 자동 복구로 수동 개입 부담 감소
- 전략 모듈 — 슬롯이 자동 복구되어 일관된 상태를 관측
- 제약사항
- 이미 released(REL-001/003) 된 KIS 어댑터/집행 경로 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 REQ) 경유가 필요하다.
- KIS `inquire-daily-ccld` 응답의 실제 `cncl_yn`(또는 동등) 필드명·값 규약을 실데이터로 확인해야 정확히 매핑할 수 있다.
- 부분 체결 후 취소는 체결분 원장 귀속과 잔여 포지션 정합을 함께 고려해야 한다(단순 롤백으로 과매도 위험).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 KIS HTS/MTS 에서 trex-core 가 낸 매도 주문을 직접 취소한다 → core 가 다음 tick 에 취소를 감지해 슬롯을 `HOLDING` 으로 복귀시키고, 필요 시 재매도(시장가)한다.
- 부분 체결 후 운영자가 잔량을 취소한다 → 체결분은 원장에 기록되고 잔여 포지션만 정합 처리되어 과매도가 발생하지 않는다.
- 어떤 이유로 pending 주문이 비정상적으로 오래 머문다 → 워치독/타임아웃이 stale 로 감지해 reconciliation 또는 경고를 발생시킨다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- KIS `poll_fill` 이 `inquire-daily-ccld` 응답의 취소여부(`cncl_yn` 등)를 파싱해, 취소된 주문(체결 0 또는 부분 체결 후 취소)을 `FillStatus(state="cancelled")` 로 매핑한다 — 기존 `is_terminal_reject` → `_rollback` 경로가 동작하도록.
- 부분 체결 후 취소(OQ-1 A안): 체결분 원장 기록 + 슬롯 qty 차감 + 잔여 HOLDING 복귀의 부분 체결 회계를 도입한다(과매도/원장 누락 방지).
- (OQ-2 — Alert-only) pending 주문 stale 워치독을 도입한다 — 임계 초과 시 WARN/메트릭 가시화만, 상태 미변경(auto-remediation 은 후속).
- 인터페이스/런타임 baseline 문서에 취소 감지·복구 흐름을 반영한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/adapters/broker/kis.py` — `poll_fill`(163~208) 에 `cncl_yn` 파싱 + `state="cancelled"` 매핑(핵심 갭)
- `trex_core/application/broker_port.py` — `FillStatus` 계약(필요 시 부분 체결+취소 표현 보강)
- `trex_core/application/execution.py` — 완전 취소(체결 0)는 기존 `_poll`/`_rollback` 경로 자동 재사용. 부분 체결+취소(OQ-1 A안)는 체결분 원장 기록 + 슬롯 qty 차감 + 잔여 HOLDING 복귀의 부분 체결 회계 경로 신규(`_on_filled`/`_record_ledger` 가 전량 체결만 다루던 것을 부분에도 확장)
- `trex_core/domain/slot/` — 슬롯 qty 부분 차감 후 HOLDING 복귀 전이(잔여 수량 갱신)
- (워치독 채택 시) pending stale 감지 컴포넌트 — 집행 reconciliation 경로
- 문서 영향:
- `docs/interface-contract.md` — KIS poll/fill 상태 매핑(취소 포함) 반영
- `docs/runtime-flows.md` — 외부 취소 감지 → 롤백 → 재매도/회복 흐름 반영
- 운영 영향:
- 외부 취소·갇힘의 자동 복구로 수동 개입·불필요 폴링 감소
- (워치독) stale 임계·알람 운영 파라미터 추가 가능
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: KIS `poll_fill` 은 `inquire-daily-ccld` 응답의 취소여부를 파싱해, 외부 취소된 주문(체결 0)을 `FillStatus(state="cancelled")` 로 매핑한다. 이로써 기존 `is_terminal_reject → _rollback` 경로가 동작해 슬롯이 `HOLDING` 으로 자동 복귀한다.
- FR-2: 부분 체결 후 취소(`0 < ccld_qty < ord_qty`)를 감지해 (OQ-1 A안) ① 체결분(`ccld_qty`)을 원장에 1회 기록, ② 슬롯 qty 를 `ccld_qty` 만큼 차감, ③ 잔여 qty 로 `HOLDING` 복귀해 잔여분만 재매도 가능하게 한다. `avg_price` 는 유지하여 잔여 포지션 정합을 보장하고 과매도/원장 누락을 방지한다.
- FR-3: (OQ-2 — Alert-only) pending 주문이 configurable 임계 시간을 초과해 머물면 stale 로 감지해 WARN 로그·메트릭으로 가시화한다. 슬롯/주문 상태는 변경하지 않는다(자동 롤백/취소는 후속). cncl_yn 매핑 오류(R-1) 등 미지 원인의 무한 갇힘을 silent 가 아니라 운영자에게 노출하는 안전망이다.
- FR-4: `docs/interface-contract.md` 와 `docs/runtime-flows.md` 에 취소 감지·복구(및 워치독) 흐름을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 자동 회복성 — 외부 취소된 슬롯이 운영자 개입 없이 다음 tick 내에 `HOLDING` 으로 복구되어 `pending_sell` 영구 갇힘이 발생하지 않아야 한다.
- NFR-2: 자원 효율 — 죽은(취소된) 주문에 대한 무한 폴링이 중단되어 불필요한 KIS API 호출이 발생하지 않아야 한다.
- NFR-3: 정합성 — 부분 체결 후 취소에서 체결분이 원장에 정확히 귀속되고 잔여 포지션이 실제 잔고와 일치해야 한다(과매도/이중매도 금지).
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- KIS `poll_fill` 의 `cncl_yn` 파싱 + `cancelled` 매핑(핵심 갭)
- 부분 체결 후 취소 처리(OQ-1 A안: 체결분 원장 기록 + qty 차감 + 잔여 HOLDING 복귀)
- pending stale 워치독(OQ-2 Alert-only: WARN/메트릭 가시화)
- 인터페이스/런타임 문서 갱신
- Out of Scope
- buy 경로·paper 브로커 동작 변경(paper 는 이미 정상)
- 매도 가격 정책(dcy-012 에서 처리 — 본 주기는 취소 *감지*만)
- 워치독 auto-remediation(타임아웃 시 자동 롤백/취소) — 정상 slow fill·지정가 대기 상호작용·KIS 취소 능력 필요로 후속 분리
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): KIS `inquire-daily-ccld` 의 실제 취소 표현(필드명/값)이 문서 가정과 다르면 취소를 여전히 `accepted` 로 오판해 갇힘이 재현된다.
- 완화 방안: 실 응답 샘플(또는 KIS 명세)로 `cncl_yn` 등 필드를 확정하고, 미지 응답은 `unknown` 으로 보수 처리하되 OQ-2 의 Alert-only 워치독으로 무한 갇힘을 가시화하는 안전망을 둔다.
- R-2 (Medium): 외부 취소 → 롤백 → 재매도가 반복되면(운영자가 계속 취소) safety-sell 이 무한 재매도 루프를 돌 수 있다.
- 완화 방안: dcy-010(REQ-052) 의 per-slot 재시도 쿨다운/영속 거부 경고가 재매도 빈도를 제어하는지 확인하고, 필요 시 cancelled 도 쿨다운 트리거에 포함한다.
- R-3 (Medium, OQ-1 A안으로 완화): 부분 체결 후 취소를 단순 롤백하면 이미 팔린 수량을 다시 팔아 과매도가 날 수 있다.
- 완화 방안: OQ-1 A안(체결분 원장 기록 + 슬롯 qty 차감 + 잔여 HOLDING 복귀)으로 잔여분만 재매도되어 과매도가 구조적으로 차단된다. 부분 체결 회계 정확성은 REQ-018(원장)·REQ-031(포지션 정합) 검증과 연계해 보장한다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-012 구현이 완료되어 슬롯 가격 필드가 `limit_price` 로 통일되고 HOLDING 진입 시 clear 되므로, 취소 후 롤백된 슬롯의 재매도는 시장가로 나가 체결성이 보장된다.
- A-2: `FillStatus.is_terminal_reject` 가 이미 `cancelled` 를 포함하고 `_rollback` 이 `PENDING_SELL → HOLDING` 을 수행하므로, KIS 어댑터가 `cancelled` 만 정확히 반환하면 application 경로 변경 없이 회복이 성립한다.
- A-3: paper 브로커는 이미 취소를 올바르게 처리하므로 본 변경은 KIS 어댑터(+필요 시 부분취소/워치독)에 국한된다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 부분 체결 후 취소 처리 정책 — 체결분을 원장에 기록하고 잔여 수량만 정합(예: HOLDING 잔량 갱신) 처리하는가, 아니면 다른 규칙을 쓰는가?
- 상태: Answered (2026-06-08, 사용자 결정 — A안: 한 번에 올바르게(정확 회계))
- 결정: 부분 체결 후 취소 감지 시 ① 체결분(`ccld_qty`)을 원장에 1회 기록, ② 슬롯 qty 를 `ccld_qty` 만큼 차감, ③ 잔여 qty 로 `HOLDING` 복귀(재매도는 잔여분만)한다. `avg_price`(주당 원가)는 그대로 유지되어 잔여 포지션도 정합하며, 과매도·원장 누락을 동시에 방지한다. 단순 롤백(C)·격리 후속(B)은 미채택.
- 반영 위치: FR-2, 4. 변경 항목, 영향 범위, R-3
- OQ-2: pending stale 워치독/타임아웃을 이번 주기에 포함하는가, 후속으로 분리하는가? 포함 시 임계 기준은?
- 상태: Answered (2026-06-08, 사용자 결정 — 포함, Alert-only)
- 결정: pending 주문이 임계 시간을 초과해 머물면 stale 로 감지해 WARN 로그·메트릭으로 **가시화만** 하고 슬롯/주문 상태는 변경하지 않는다(Alert-only). cncl_yn 매핑이 틀려도(R-1) 무한 갇힘이 silent 가 아니라 운영자에게 노출되는 안전망. 타임아웃 시 자동 롤백/취소(auto-remediation)는 정상 slow fill·지정가 대기와의 상호작용 및 KIS 주문 취소 능력이 필요해 후속으로 분리. 임계는 configurable(시장가 매도는 짧게, 지정가 매도는 관대하게 또는 단일 보수적 임계).
- 반영 위치: FR-3, 범위 경계, R-1 완화
- OQ-3: 거버넌스 경로 — 기존 REQ(REQ-012 KIS 어댑터, REQ-014 reconciliation 등)의 change-req 로 처리할지, 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-012(KIS 어댑터: `cncl_yn` 파싱 + `cancelled` 매핑), REQ-013(주문 집행 루프: 부분 체결 회계 — qty 차감 + 잔여 HOLDING 복귀), REQ-018(체결→원장 기록: 부분 체결분 1회 기록), REQ-031(포지션 정합: 잔여 포지션 정합 연계), REQ-014(주문 핸드오프 durability + reconciliation: pending stale 워치독 Alert-only), REQ-015(브로커/집행 baseline 문서 정합: interface-contract/runtime-flows 갱신). 원칙적으로 신규 REQ 는 생성하지 않는다 — 단 워치독 임계 env 설정이 별도 추적을 요하면 REQ 정제에서 소형 config 신규 REQ 를 둘 수 있다(조건부).
- 반영 위치: 1. 거버넌스 해석, R-1 완화 방안, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 외부 취소된 매도 주문이 다음 tick 에 `cancelled` 로 감지되어 슬롯이 `HOLDING` 으로 복귀하고 `broker_order_id` 가 비워진다(영구 갇힘 0건).
- S-2: 취소된(죽은) 주문에 대한 추가 KIS 폴링이 발생하지 않는다.
- S-3: 부분 체결 후 취소 시 체결분이 원장에 1회 기록되고 잔여 포지션이 실제 잔고와 일치한다(과매도/이중기록 0건).
- S-4: pending 주문이 임계 시간을 초과하면 워치독이 WARN/메트릭으로 가시화한다(미지 원인 무한 갇힘이 silent 로 남지 않음). 슬롯/주문 상태는 변경하지 않는다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: KIS 어댑터 단위 테스트(취소 응답 fixture → `cancelled`) + 집행 통합 테스트(롤백→HOLDING) 통과 / 데이터 출처: `poll_fill` 테스트, 슬롯 상태 스냅샷, `inquire-daily-ccld` 응답 fixture
- S-2 측정 방식: 취소 감지 후 해당 slot 의 poll 호출 중단 확인 / 데이터 출처: 폴링 호출 로그/카운터
- S-3 측정 방식: 부분 체결+취소 시나리오 테스트에서 원장 1건·잔여 수량 정합 확인 / 데이터 출처: ledger_outbox/포지션 정합(REQ-031) 검증, 슬롯 qty
- S-4 측정 방식: 임계 초과 pending 주입 테스트에서 WARN/메트릭 발생 및 상태 미변경 확인 / 데이터 출처: 워치독 테스트, 데몬 로그/메트릭, 슬롯 상태 스냅샷
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 부분취소 정책(OQ-1 A안)·워치독 포함 여부(OQ-2 Alert-only)·거버넌스(OQ-3) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(취소 필드 규약 확인)의 change-req 대상(REQ-012/013/018/031/014/015)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정·DATA(취소 필드 확인)는 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md
- 참조 문서:
- GitHub issue #9 (fourblocks2020/trex-core) — KIS 외부 취소 시 pending_sell 갇힘
- docs/discovery/dcy-003_20260604_broker-execution-and-outbox.md — 집행 폴/롤백 경로 도입 근거
- docs/discovery/dcy-012_20260608_sell-order-pricing-entry-price-bug.md — 매도 가격 버그(같은 pending_sell 갇힘 증상의 다른 원인, A-1)
- docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md — per-slot 재시도 쿨다운(재매도 루프 완화, R-2)
- trex_core/adapters/broker/kis.py:163-208 — `poll_fill` 취소 미파싱 갭
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 부분 체결 후 취소 = A안(체결분 원장 기록 + 슬롯 qty 차감 + 잔여 HOLDING 복귀, 정확 회계) / OQ-2 워치독 = 포함(Alert-only: WARN/메트릭 가시화, auto-remediation 후속). 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-012/013/018/031/014/015 change-req, 신규 REQ 원칙적 없음).
## DATA
- (R-1 연계, REQ/delivery 단계 확인) KIS `inquire-daily-ccld` 응답의 실제 취소 표현(필드명 `cncl_yn` 등·값 규약). 구현/검증 단계에서 실 응답·KIS 명세로 확정할 데이터이며, 미지 응답은 `unknown` 보수 처리 + 워치독 가시화로 안전망이 있어 Discovery confirm 을 차단하지 않는다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (부분 체결 후 취소=A안 정확 회계 / 워치독=포함 Alert-only / 거버넌스=REQ-012/013/018/031/014/015 change-req, 신규 REQ 원칙적 없음). C 유형 플레이스홀더 0개, 저장소 사실관계 일치. DATA(KIS 취소 필드)는 REQ/delivery 단계 확인 비차단. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 14:10 KST
@@ -1,225 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-014_20260608_trading-date-configurable-timezone-kst
- 이슈명: 당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation
- 작성 시각(KST): 2026-06-08 13:58 KST
- 마지막 갱신 시각(KST): 2026-06-08 17:20 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-005/048/009 (전부 change-req, 신규 REQ 없음 — OQ-3; 스케줄러는 REQ-030 SnapshotScheduler 패턴 준용). 각 CHG-20260608-01. delivery: BAT-018
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #14 (fourblocks2020/trex-core)
- 해석:
- daemon 이 시작 시 `datetime.now(tz=timezone.utc).date()` 기준으로 슬롯을 1회 preallocate 해, `trading_date` 가 UTC 날짜로 생성되어 KST 기준 "오늘 슬롯"과 어긋나고, 재시작 없이는 다음날 슬롯이 자동 생성되지 않는 문제를 정식 SDLC 주기로 착수한다.
- 이번 Discovery에서는 `trading_date` 계산 기준을 설정 가능한 타임존(기본 `Asia/Seoul`)으로 분리하고, timestamp 저장은 UTC 유지하며, 일자 경계에 맞춰 다음날 슬롯이 자동 생성되도록 일일 preallocation 을 보강하는 범위를 판정한다. 이는 daemon 런타임/스케줄러/설정 baseline 변경이므로, 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-005/048/009, 스케줄러는 REQ-030 패턴 준용) `change-req` 로 처리하고 신규 REQ 는 원칙적으로 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-013) 중 `trading_date` 타임존 기준이나 일자 경계 일일 preallocation 을 다룬 문서는 없다. dcy-002(슬롯 코어 데몬화)는 부트스트랩 시 preallocation 을 도입했으나 타임존·일자 경계 자동화를 정의하지 않았다.
- 변경이 이미 Implemented·released 된 daemon 부트스트랩(`setup`)·슬롯 도메인(`trading_date`)·설정 계약의 의미를 바꾸고, 일일 스케줄링이라는 런타임 동작을 추가하므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/runtime/daemon.py` 의 `setup()`(daemon.py:291~)은 `self.slot_service.preallocate_daily_slots(datetime.now(tz=timezone.utc).date(), self.config.total_slots)`(daemon.py:293~294)로, **UTC 날짜** 기준 슬롯을 데몬 시작 시 1회만 생성한다.
- `preallocate_daily_slots`(`trex_core/domain/slot/service.py:42~53`)는 동일 `trading_date` 에 대해 멱등(2회차 이후 0 반환)이다. 즉 같은 날짜에 대한 재실행은 안전하나, **날짜가 바뀌어도 자동으로 다음날 슬롯을 만들지 않는다**(재시작 의존).
- timestamp 저장은 UTC 다 — 슬롯 `created_at`/`updated_at`·전이 로그가 UTC. 그러나 `trading_date` 도 UTC 날짜라 KST 자정~오전 구간에 KST 캘린더 일자와 어긋난다.
- 타임존을 설정으로 분리하는 경로는 아직 없다 — `trex_core/runtime/config.py` 에 `trading_date` 용 타임존 설정이 없다. 다만 설정 인프라는 이미 두 표면을 갖춘다: env(`.env`, `TREX_*`, 대부분 설정)와 TOML(`DaemonInstanceConfig.from_toml`, `--config <path>.toml`, REQ-048 per-instance). 즉 새 운영 설정은 기존 TOML 에 키를 추가하면 된다.
- 유사 패턴은 이미 존재한다 — `trex_core/application/snapshot_scheduler.py` 의 `SnapshotScheduler` 는 `_KST` 로 `now_kst.date()` 를 구해 **KST 일자당 1회** fire 하고(`_last_fired_date.date() == now_kst.date()`), tick 루프(`tick_sec`)로 동작한다. 일일 KST 경계 작업의 직접적인 선례다.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. daemon 부트스트랩/스케줄러는 runtime flow 에 속하므로 이번 변경은 `docs/runtime-flows.md` 갱신을 동반한다.
- 기대 상태
- `trading_date` 가 설정 가능한 타임존(기본 `Asia/Seoul`) 기준으로 계산되어, KST "오늘 슬롯" 과 DB `trading_date` 가 일치한다.
- timestamp(`created_at`/`updated_at`/전이 ts)는 UTC 저장을 유지한다.
- daemon 재시작 없이도 TZ 일자 경계에서 다음날 슬롯이 누락 없이 자동 생성된다(멱등 일일 preallocation).
- 운영자가 타임존을 TOML 설정으로 명시 지정할 수 있다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 타임존·스케줄링·설정 계약 변경 승인
- 운영 담당자 — 일자 경계 슬롯 생성·타임존 설정 운영
- 전략 모듈/테스트 — KST 기준 "오늘 슬롯" 을 일관되게 관측
- 제약사항
- 이미 released 된 daemon 부트스트랩·슬롯 도메인·설정 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 REQ) 경유가 필요하다.
- timestamp 는 UTC 를 유지하고 `trading_date` 만 TZ 기준으로 분리해, 저장 일관성과 일자 해석을 명확히 구분해야 한다.
- 다중 인스턴스(dcy-009) 운영 시 일일 preallocation 이 중복 생성되지 않도록 멱등성에 의존한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 KST 오전에 "오늘 슬롯" 을 조회한다 → DB `trading_date` 가 KST 당일과 일치해 직관적으로 해석된다(UTC 자정 경계 혼동 제거).
- daemon 이 며칠간 재시작 없이 떠 있다 → TZ 일자 경계마다 다음날 슬롯이 자동 생성되어 매일 거래 준비가 누락되지 않는다.
- 운영자가 다른 거래소/타임존을 쓰는 환경에서 TOML 로 타임존을 명시 설정한다 → 시스템 시간과 무관하게 일관된 `trading_date` 가 계산된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- `trading_date` 계산 기준을 시스템 UTC 가 아니라 설정 가능한 타임존(기본 `Asia/Seoul`)으로 분리한다. timestamp 저장은 UTC 유지.
- daemon 시작 1회 preallocation 을 TZ 일자 경계 기준 일일 preallocation 으로 보강한다 — `SnapshotScheduler` 패턴(tick 루프 + TZ 일자당 1회 fire)을 따르고 기존 멱등성을 활용한다.
- 본 기능의 운영 설정(타임존 + 일일 트리거 시각)을 기존 TOML 설정(REQ-048 `DaemonInstanceConfig`, `--config <path>.toml`)에 추가하고, `docs/runtime-flows.md`·`docs/data-model.md` 에 trading_date 타임존·일일 생성 흐름을 반영한다. (이 프로젝트는 이미 env[.env, 대부분]와 TOML[per-instance, REQ-048]을 병용하므로, 본 설정은 TOML 측으로 자연스럽게 편입된다.)
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/runtime/config.py` — trading_date 타임존 + 일일 트리거 시각을 기존 TOML 설정(`DaemonInstanceConfig.from_toml`, REQ-048)에 추가(기본 `Asia/Seoul`/00:00). 신규 설정 표면이 아니라 기존 `--config <path>.toml` 에 키 추가
- `trex_core/runtime/daemon.py` — `setup()` 의 UTC 날짜 preallocation 을 TZ 기준으로 변경 + 일일 preallocation 루프/스케줄러 배선
- `trex_core/application/` — 일일 preallocation 스케줄러(또는 `SnapshotScheduler` 유사 컴포넌트). 기존 멱등 `preallocate_daily_slots` 재사용
- `trex_core/domain/slot/service.py` — `trading_date` 를 TZ 기준 date 로 받는 호출부 정합(도메인 시그니처는 `date` 유지)
- 문서 영향:
- `docs/runtime-flows.md` — daemon 부트스트랩/일일 preallocation·trading_date 타임존 흐름 반영
- `docs/data-model.md` — `trading_date`(TZ 기준 거래일) vs timestamp(UTC) 구분 명시
- 운영 영향:
- 타임존·트리거 시각 TOML 설정 추가 → 배포/운영 문서 안내 필요
- 일자 경계 자동 생성으로 재시작 의존 제거
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `trading_date` 는 설정 가능한 타임존(기본 `Asia/Seoul`) 기준 날짜로 계산한다. 시스템 UTC 날짜에 의존하지 않는다.
- FR-2: timestamp(`created_at`/`updated_at`/전이 ts)는 UTC 저장을 유지한다(거래일과 시각 저장 분리).
- FR-3: daemon 재시작 없이 TZ 일자 경계에서 다음날 슬롯을 자동 생성한다 — tick 루프로 TZ 일자당 1회 멱등 preallocation(`SnapshotScheduler` 패턴 준용). 생성 트리거 시각은 기본 **00:00(TZ 자정)** 이며, TOML 설정으로 변경 가능하다(OQ-1).
- FR-4: 운영자가 타임존을 TOML 설정으로 명시 지정할 수 있다(기본 `Asia/Seoul`). 본 기능의 운영 설정(타임존 + 트리거 시각)은 TOML 로 일원화한다.
- FR-5: `docs/runtime-flows.md` 와 `docs/data-model.md` 에 trading_date 타임존·일일 preallocation 흐름을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 저장 일관성 — 모든 timestamp 는 UTC, `trading_date` 만 TZ 기준으로 분리되어 해석 혼동이 없어야 한다.
- NFR-2: 멱등성 — 일일 preallocation 이 같은 `trading_date` 에 중복 슬롯을 만들지 않아야 한다(다중 인스턴스·중복 tick 안전).
- NFR-3: 자동화 안정성 — 날짜 경계에서 다음날 슬롯 생성이 누락되지 않고, daemon 재시작 없이 동작해야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- `trading_date` 의 설정 가능한 타임존 분리(기본 KST) + timestamp UTC 유지
- TZ 일자 경계 기준 일일 멱등 preallocation
- 타임존·트리거 시각 TOML 설정 + runtime-flows/data-model 문서 갱신
- Out of Scope
- timestamp 저장 형식 변경(UTC 유지)
- 슬롯 용량/total_slots 정책 변경
- (OQ-2 결정) 거래일 캘린더(주말·공휴일 비거래일 제외) — 매 캘린더일 생성으로 확정, 캘린더 도입은 후속 분리
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Low, OQ-1 결정으로 완화): 일일 preallocation 트리거 시점이 운영 기대와 어긋날 수 있다.
- 완화 방안: 기본 00:00(TZ 자정) + TOML 설정(OQ-1)으로 운영자가 조정 가능하게 하고, `SnapshotScheduler` 의 KST 일자당 1회 fire 패턴을 재사용해 검증 가능하게 한다.
- R-2 (Medium): 타임존 분리 전환 시점에 기존 UTC 날짜로 만들어진 슬롯과 새 TZ `trading_date` 가 경계 구간에서 불일치할 수 있다.
- 완화 방안: 전환 시 당일 경계 동작을 문서화하고, 멱등성으로 중복을 막되 경계 케이스를 테스트로 확인한다.
- R-3 (Low, OQ-2 결정으로 수용): 비거래일(주말/공휴일)에도 슬롯이 생성되어 idle 슬롯이 쌓일 수 있다.
- 완화 방안: OQ-2 결정에 따라 매 캘린더일 생성을 수용한다 — idle 슬롯은 멱등 생성이고 buy intent admit 시에만 활성화되어 무해하다. 거래일 캘린더(외부 데이터)는 후속 분리.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: `SnapshotScheduler`(snapshot_scheduler.py) 가 이미 `_KST` 기준 일자당 1회 fire + tick 루프를 구현하므로, 일일 preallocation 도 동일 패턴/유사 컴포넌트로 안전하게 구현할 수 있다.
- A-2: `preallocate_daily_slots` 가 `trading_date` 멱등이므로, 일일 재실행·다중 인스턴스에서도 중복 생성이 발생하지 않는다.
- A-3: 도메인의 `trading_date: date` 시그니처는 유지하고, 타임존 해석은 호출부(daemon/스케줄러)에서 수행하면 도메인 변경을 최소화할 수 있다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 일일 preallocation 트리거 시점 — TZ 자정(00:00) 경계에 생성하는가, 장 시작 전 특정 시각(설정)에 생성하는가?
- 상태: Answered (2026-06-08, 사용자 결정 — 기본 00:00(TZ 일자 경계), TOML 로 설정 가능)
- 결정: 일일 preallocation 은 설정 타임존의 **자정(00:00)** 일자 경계에 다음날 슬롯을 생성하는 것을 기본값으로 하고, 트리거 시각은 **TOML 설정**으로 운영자가 변경할 수 있다(`SnapshotScheduler` 의 KST 일자당 1회 fire 패턴 준용). 본 기능의 운영 설정(타임존 + 트리거 시각)은 TOML 로 일원화한다(FR-4 함께 결정). 이 프로젝트는 이미 env 와 TOML(REQ-048 per-instance)을 병용하므로, 본 설정은 기존 TOML(`--config`)에 키를 추가하는 형태로 편입된다(신규 설정 표면 아님).
- 반영 위치: FR-3, FR-4, R-1, 4. 변경 항목/영향 범위
- OQ-2: 비거래일(주말/공휴일) 처리 — 매 캘린더일 슬롯을 생성하는가, 거래일만 생성하는가(거래일 캘린더 도입)?
- 상태: Answered (2026-06-08, 사용자 결정 — A안: 매 캘린더일 생성)
- 결정: 거래일 캘린더를 도입하지 않고 매 캘린더일에 슬롯을 생성한다. 휴장일 idle 슬롯은 무해하다 — 멱등 생성이고, 슬롯은 buy intent 가 admit 될 때만 활성화되므로 휴장일엔 비어 있다가 사라지며 safety-sell·워치독 등은 holding/pending 슬롯만 대상으로 한다. KRX 공휴일 캘린더(외부 데이터·매년 유지보수)는 idle 슬롯 회피 이득 대비 비용이 커 후속으로 분리한다(필요 시: 주말 스킵 또는 거래일 캘린더).
- 반영 위치: 범위 경계(Out of Scope), R-3
- OQ-3: 거버넌스 경로 — 기존 REQ(데몬 부트스트랩·슬롯 도메인·설정)의 change-req 로 처리할지, 일일 스케줄러를 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-005(슬롯 코어 데몬 런타임 부트스트랩: `setup()` 의 UTC→TZ trading_date 계산 + 일일 preallocation 루프 배선), REQ-048(per-instance TOML 설정 로더: trading_date 타임존 + 일일 트리거 시각 키 추가), REQ-009(데몬/API/영속화 baseline 문서 정합: runtime-flows/data-model 갱신). 일일 preallocation 스케줄러는 REQ-030(일일 스냅샷 스케줄러)의 `SnapshotScheduler` 패턴을 준용한다. `preallocate_daily_slots` 가 이미 date 멱등이고 호출부(daemon)에서 TZ 계산을 수행하므로 REQ-002(slot 도메인) 본문 변경은 원칙적으로 불필요(정제에서 확인). 신규 REQ 는 원칙적으로 생성하지 않으며, 일일 preallocation 을 별도 컴포넌트로 분리해야 한다고 판단되면 REQ 정제에서 소형 신규 REQ 를 둘 수 있다(조건부).
- 반영 위치: 1. 거버넌스 해석, R-1 완화 방안, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 설정 타임존(기본 KST) 기준 "오늘 날짜" 와 생성된 슬롯의 DB `trading_date` 가 일치한다(UTC 자정 경계 포함).
- S-2: 모든 timestamp(`created_at`/`updated_at`/전이 ts)가 UTC 로 저장됨이 유지된다.
- S-3: daemon 재시작 없이 TZ 일자 경계를 넘기면 다음날 `trading_date` 슬롯이 자동 생성되고, 중복 생성은 0건이다.
- S-4: 운영자가 TOML 로 타임존을 설정하면 그 타임존 기준으로 `trading_date` 가 계산된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: KST 자정 전후 시점을 고정 clock 으로 주입한 단위 테스트에서 `trading_date` 일치 확인 / 데이터 출처: 슬롯 row `trading_date`, 주입 clock
- S-2 측정 방식: 슬롯 생성/전이 후 timestamp 컬럼 UTC 확인 / 데이터 출처: `created_at`/`updated_at`/transition_log
- S-3 측정 방식: 일자 경계를 넘기는 clock 주입 테스트에서 다음날 슬롯 1회 생성·재호출 0건(멱등) 확인 / 데이터 출처: preallocation 스케줄러 테스트, 슬롯 count
- S-4 측정 방식: 타임존 TOML 설정 케이스(KST/타 TZ) 테스트에서 `trading_date` 계산 확인 / 데이터 출처: config 로드, 슬롯 `trading_date`
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 트리거 시점(OQ-1)·거래일 캘린더(OQ-2)·거버넌스(OQ-3) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 전환 경계(R-2)의 change-req 대상(REQ-005/048/009, 스케줄러 REQ-030 패턴)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-014_20260608_trading-date-configurable-timezone-kst.md
- 참조 문서:
- GitHub issue #14 (fourblocks2020/trex-core) — 당일 슬롯 생성 기준 시간 KST 분리
- docs/discovery/dcy-002_20260603_slot-core-daemon-http-api.md — 데몬 부트스트랩·preallocation 도입 근거
- trex_core/runtime/daemon.py:291-294 — UTC 날짜 1회 preallocation(현행 갭)
- trex_core/application/snapshot_scheduler.py — KST 일자당 1회 fire 스케줄러 선례(A-1)
- docs/runtime-flows.md, docs/data-model.md — 런타임/데이터 baseline 갱신 대상
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 일일 preallocation 트리거 = 기본 00:00(TZ 자정 일자 경계), TOML 설정 가능(타임존도 TOML 일원화) / OQ-2 비거래일 = 매 캘린더일 생성(거래일 캘린더 후속 분리). 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-005/048/009 change-req, 스케줄러는 REQ-030 패턴 준용, 신규 REQ 원칙적 없음).
## DATA
- (비차단, REQ/delivery 단계) 기본 타임존 `Asia/Seoul` 확정과 TOML 키/스키마 명명 규약. 운영 정책·키 명명은 REQ 정제/설계에서 확정하며, 기본값 KST 가 본 Discovery 의 FR/성공 기준을 충분히 정의하므로 confirm 을 차단하지 않는다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (트리거=기본 00:00 TZ 자정·TOML 설정[타임존 포함] / 비거래일=매 캘린더일 생성 / 거버넌스=REQ-005/048/009 change-req, 스케줄러 REQ-030 패턴, 신규 REQ 원칙적 없음). C 유형 플레이스홀더 0개, 저장소 사실관계 일치(TOML 설정 인프라 정정 반영). DATA(기본 TZ·TOML 키 명명)는 REQ/delivery 단계 비차단. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 14:50 KST
@@ -1,229 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap
- 이슈명: portfolio-hub 404(미등록 포트폴리오)가 retryable 로 분류되어 백오프·상한 없이 영구 재시도됨
- 작성 시각(KST): 2026-06-08 14:21 KST
- 마지막 갱신 시각(KST): 2026-06-08 18:10 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-017/019/018/020 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-018=CHG-20260608-03, REQ-017/019/020=CHG-20260608-01. 백오프 env 는 REQ-019 에 흡수. delivery: BAT-019
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #13 (fourblocks2020/trex-core), 관련 issue #11(dcy-011, ledger silent skip)
- 해석:
- portfolio-hub 가 미등록 포트폴리오에 404 를 반환하면 ledger outbox 가 이를 `LedgerRetryableError`(일시적 실패)로 분류해 **백오프·상한·터미널 상태 없이 초당 1회 영구 재시도**하는 버그를 정식 SDLC 주기로 착수한다(실측: buy 행 930회 재시도).
- 이번 Discovery에서는 (1) 영속(permanent) 오류(404 등 명백한 클라이언트 오류) 분류 분리, (2) retryable 에 지수 백오프 + max-attempts + dead-letter, (3) 터미널 실패 가시화의 범위를 판정한다. 이미 Implemented·released 된 ledger 어댑터/outbox 계약 변경이므로, 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-017/019/018/020) `change-req` 로 처리하고 신규 REQ 는 원칙적으로 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-014) 중 ledger outbox 의 영속 오류 분류·백오프·상한·dead-letter 를 다룬 문서는 없다. dcy-004(portfolio-hub ledger 연동)는 경로를 도입했고, dcy-010(safety-sell 영속 거부 backoff)은 **다른 경로**(safety-sell→KIS)의 per-slot 쿨다운이며, dcy-011(issue #11)은 portfolio_id 누락 skip 가시화로 본 이슈(전송 단계 404)와 원인이 다르다.
- 변경이 이미 Implemented·released 된 LedgerPort/portfolio-hub 어댑터(REQ-017)와 outbox durability(REQ-019)의 오류 분류·상태 계약을 바꾸므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/adapters/ledger/portfolio_hub.py`(79~95)는 응답을 분류할 때 2xx→성공, 409→`LedgerConflictError`(터미널), **그 외 4xx/5xx 전부→`LedgerRetryableError`** 로 보낸다(파일 헤더 계약: `4xx (other)/5xx => LedgerRetryableError`). 따라서 404(portfolio 없음)처럼 **재시도로 절대 해소되지 않는 영속 오류**가 일시적 오류와 동일하게 취급된다.
- `trex_core/application/ledger_outbox.py`(137~142)는 `LedgerConflictError`→`conflict_blocked`(터미널), `LedgerRetryableError`→행을 `pending` 유지 + `attempts += 1` 이 전부다. **지수 백오프·max-attempts 상한·dead-letter 상태가 없다.** ledger 루프는 `ledger_interval_sec`(기본 1.0초)마다 `list_pending(limit=32)` 를 FIFO 로 다시 집어 재시도한다.
- `trex_core/adapters/persistence/sql/002_create_ledger_outbox.sql`(20~21)의 status CHECK 는 `pending`/`completed`/`conflict_blocked` 3개뿐이다. 즉 비-409 영속 실패를 담을 **터미널 실패 상태가 스키마에 없다**.
- 결과(실측, instance `trex-admin-daily-trading`, `portfolio_id=PF-test` 미등록): buy 행 `attempts=930`, sell 행 `attempts=9`, 둘 다 `status=pending last_outcome=retryable_failure`. 멈출 때까지 초당 1회 영구 호출.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`)는 존재하며 최신이다. ledger outbox 처리는 기존 runtime flow 에 포함된다.
- 기대 상태
- 404 등 명백한 클라이언트(영속) 오류는 retryable 이 아니라 **터미널 실패 상태**로 분류되어 재시도가 즉시 중단된다.
- 진짜 일시적 오류(5xx·네트워크·429 등)는 **지수 백오프 + max-attempts** 로 재시도하되, 상한 초과 시 **dead-letter** 로 전이되어 무한 1초 재시도가 사라진다.
- 터미널/dead-letter 실패가 WARN 로그·메트릭과 구분되는 status 로 가시화되어, 운영자가 "기록이 영영 안 됨" 을 인지하고 head-of-line 잠식이 해소된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — ledger 오류 분류·outbox 상태 계약 변경 승인
- 운영 담당자 — 영속 실패 가시화·미등록 포트폴리오 조치
- portfolio-hub 운영 — 무한 404 호출 spam 제거로 부하 경감
- 제약사항
- 이미 released 된 LedgerPort/portfolio-hub 어댑터·outbox durability 변경이므로 ad-hoc 수정이 아니라 정식 SDLC(change-req 또는 신규 REQ) 경유가 필요하다.
- outbox status enum 확장은 스키마 마이그레이션을 동반하며 기존 `conflict_blocked` 터미널 패턴과 정합해야 한다.
- 영속 오류 분류가 과하면(예: 등록 지연으로 인한 일시적 404) 정상 거래의 ledger 를 영구 실패시킬 수 있어 분류 경계가 중요하다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 미등록 portfolio_id 로 거래를 발생시킨다 → ledger 전송이 404 → 즉시 터미널 실패로 표시되고 WARN/메트릭으로 노출되어, 초당 무한 재시도 없이 운영자가 portfolio 등록 등 조치한다.
- portfolio-hub 가 일시적으로 5xx/네트워크 장애를 낸다 → ledger outbox 가 지수 백오프로 재시도하고, max-attempts 초과 시 dead-letter 로 전이해 무한 재시도를 막는다.
- 영속 실패 행이 다수 쌓여도 FIFO 배치(limit=32)를 잠식하지 않아 신규 ledger 행이 지연 없이 처리된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- ledger 어댑터(portfolio-hub) 오류 분류를 분리한다 — 404(및 명백한 클라이언트 오류)를 retryable 이 아니라 **영속 오류(LedgerPermanentError 등)** 로 매핑해 outbox 가 터미널 실패로 처리하게 한다.
- outbox retryable 처리에 **지수 백오프 + jitter + max-attempts** 를 도입하고, 상한 초과 시 단일 터미널 상태 `delivery_failed`(dead-letter, 사유 저장)로 전이한다. `list_pending` 이 `next_retry_at` 미도래 행을 건너뛰어 head-of-line 잠식을 막는다.
- outbox status enum 에 단일 터미널 `delivery_failed` 추가 + `next_retry_at` 컬럼 스키마 마이그레이션. (영속·dead-letter 공용, 사유로 구분)
- 터미널/dead-letter 실패를 WARN 로그·메트릭으로 가시화한다(dcy-011 #11 가시화 패턴 정합).
- ledger baseline 문서에 오류 분류·백오프·터미널 상태 흐름을 반영한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/adapters/ledger/portfolio_hub.py` — 404/400/422 등 영속 오류 분류(신규 `LedgerPermanentError` 매핑), 계약 헤더 갱신
- `trex_core/application/ledger_outbox.py` — 영속 오류→`delivery_failed` 터미널, retryable 지수 백오프+jitter + max-attempts → `delivery_failed`(dead-letter), `list_pending` 의 `next_retry_at` 스킵
- `trex_core/adapters/persistence/sql/` — ledger_outbox status enum 에 `delivery_failed` 추가 + `next_retry_at` 컬럼 마이그레이션
- `trex_core/runtime/config.py` — 백오프 base/cap·max-attempts env 설정(dcy-010 관례)
- LedgerPort 계약(에러 타입) — 영속 오류 타입 추가
- 문서 영향:
- `docs/runtime-flows.md` — ledger outbox 오류 분류·백오프·터미널/dead-letter 흐름 반영
- `docs/data-model.md` — ledger_outbox status enum 확장 반영
- `docs/interface-contract.md` — ledger 어댑터 오류 분류 계약(필요 시)
- 운영 영향:
- 미등록 포트폴리오의 무한 404 spam 제거 → portfolio-hub 부하·로그 오염 경감
- 터미널/dead-letter 가시화 + 운영자 재처리 경로 필요
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: portfolio-hub 어댑터는 영속(클라이언트) 오류를 retryable 과 분리한다 — **404·400·422**(OQ-1)를 `LedgerPermanentError`(가칭)로 매핑하고, outbox 는 이를 터미널 실패 상태로 처리해 재시도하지 않는다. 408·429·5xx·네트워크·(보수적으로)401/403 은 retryable 로 유지한다.
- FR-2: 진짜 일시적 오류(5xx·네트워크·408/429 등)는 **지수 백오프 + jitter**(`delay = min(base × 2^(attempts-1), cap)`, `next_retry_at` 기준)로 재시도하고, **max-attempts** 초과 시 터미널 `delivery_failed`(dead-letter)로 전이한다. 백오프 base/cap·max-attempts 는 env 설정(dcy-010 관례). 무한 1초 재시도를 제거한다.
- FR-3: ledger_outbox 에 **단일 터미널 실패 상태 `delivery_failed`** 를 추가(영속·dead-letter 공용, 사유 저장)하고 `next_retry_at` 컬럼 + status enum 마이그레이션을 추가한다. `list_pending` 은 `next_retry_at > now`(백오프 중) 행을 건너뛰어 FIFO head-of-line 잠식을 막는다.
- FR-4: 터미널/dead-letter 실패 발생 시 WARN 로그 + 메트릭으로 가시화하고, status 로 정상 `pending` 과 구분되게 한다(issue #11/dcy-011 가시화 패턴 정합).
- FR-5: `docs/runtime-flows.md`·`docs/data-model.md`(및 필요 시 `docs/interface-contract.md`)에 오류 분류·백오프·터미널 상태 흐름을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 외부 호출 절제 — 영속 오류는 재시도하지 않고, 일시적 오류는 백오프로 호출 빈도를 제한해 portfolio-hub spam 이 발생하지 않아야 한다.
- NFR-2: 가시성 — ledger 가 끝내 기록되지 않는 실패는 메모리 카운터나 무성 pending 이 아니라 status·로그·메트릭으로 영구 관측 가능해야 한다(무성 영속 실패 금지).
- NFR-3: 처리량 보호 — 영속/백오프 대기 행이 FIFO 배치를 잠식해 신규 ledger 행 처리를 지연시키지 않아야 한다.
- NFR-4: 정합성 — 상태 전이(pending↔backoff↔terminal/dead-letter)와 멱등(correlation_id, REQ-019)이 깨지지 않아야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 영속 오류 분류 분리(404 등 → 터미널)
- retryable 지수 백오프 + max-attempts + dead-letter + list_pending 백오프 스킵
- 터미널/dead-letter 가시화(로그/메트릭/status)
- status enum 마이그레이션 + ledger baseline 문서 갱신
- Out of Scope
- safety-sell→KIS 영속 거부 backoff(dcy-010/REQ-052 에서 별도 처리 — 경로 다름)
- portfolio_id 누락 ledger skip(dcy-011/#11 에서 처리)
- dead-letter 행의 재처리 트리거(운영 명령/자동 재처리) — 이번 주기는 재처리 가능한 dead-letter 상태 제공까지이며, 재처리 트리거/자동화는 후속 분리(issue #13 핵심 해소에 불요)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Medium, OQ-1 으로 완화): 영속 오류 분류가 과하면 정상 거래의 ledger 가 영구 실패로 버려질 수 있다.
- 완화 방안: OQ-1 으로 permanent 범위를 404/400/422 로 한정했고, dcy-011 의 admission-time portfolio_id 검증으로 ledger 404 의 등록-지연 false-positive 가능성이 크게 줄었다. 잔여 false-positive 도 dead-letter 가 폐기가 아니라 **운영자 재처리 가능 상태**라 복구 가능하다.
- R-2 (Medium): status enum 확장 마이그레이션이 기존 `conflict_blocked`/`pending`/`completed` 와 정합하지 않으면 outbox 처리가 깨질 수 있다.
- 완화 방안: 기존 터미널(`conflict_blocked`) 패턴을 그대로 따르는 새 터미널 상태를 추가하고, 마이그레이션·상태 전이를 테스트로 검증한다.
- R-3 (Medium): dead-letter/터미널 행이 단순 포기되면 체결은 됐는데 ledger 영구 누락(P&L 왜곡)이 고착된다.
- 완화 방안: dead-letter 를 운영자 재처리 가능 상태로 두고, dcy-011 의 `find_ledger_gap_slots`(REQ-055)·skip 가시화와 연계해 누락을 추적·복구한다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-011(#11) 이 ledger skip 가시화(WARN+카운터, REQ-018)와 `find_ledger_gap_slots`(REQ-055) 를 이미 구현했으므로, 본 이슈의 터미널 실패 가시화도 동일 패턴으로 정합하게 추가할 수 있다.
- A-2: dcy-010(REQ-052/053) 의 백오프/쿨다운 + env 설정 관례가 있어, ledger 백오프 정책의 파라미터·설정 방식을 유사하게 설계할 수 있다(경로는 다름).
- A-3: 409 가 이미 `conflict_blocked` 터미널 상태로 처리되므로, 영속/ dead-letter 도 같은 터미널 상태 패턴으로 추가하면 outbox 처리 구조 변경을 최소화할 수 있다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 영속(permanent) 분류 범위 — 404 만 터미널로 보는가, 400/422 등 명백한 클라이언트 오류도 포함하는가? 408/429 같은 일시성 4xx 는 retryable 유지가 맞는가?
- 상태: Answered (2026-06-08, 사용자 결정 — 404/400/422 영속)
- 결정: **404·400·422 = 영속(터미널, 재시도 0)**, **408·429·5xx·네트워크 = retryable + 백오프**, 401/403 은 보수적으로 retryable+백오프(토큰 일시 갱신 가능성). 근거: dcy-011 이 admission 에서 portfolio_id 를 universe 검증(`unknown_portfolio` 거부)하므로 ledger 단계 404 는 타이밍 lag 이 아니라 portfolio-hub 미등록 = 진짜 config 갭 → 재시도로 해소 불가. 400/422 도 동일 payload 재전송으로 풀리지 않음.
- 반영 위치: FR-1, R-1, DATA
- OQ-2: 백오프·상한 정책 — 지수 백오프 파라미터(base/max), max-attempts 값, dead-letter 상태명·전이 조건은?
- 상태: Answered (2026-06-08, 사용자 결정 — 추천안 채택)
- 결정: (1) **지수 백오프 + jitter** — `delay = min(base × 2^(attempts-1), cap)`, `next_retry_at` 컬럼 추가, `list_pending` 은 `next_retry_at > now` 행을 건너뜀(head-of-line 해소). (2) **max-attempts** 초과 시 터미널 전이. 백오프 base/cap·max-attempts 는 **env 설정**(dcy-010 REQ-052/053 관례; 예: `TREX_LEDGER_RETRY_BASE_SEC`/`_CAP_SEC`/`TREX_LEDGER_MAX_ATTEMPTS`, 구체 default 는 REQ 정제에서 확정). (3) **단일 터미널 상태 `delivery_failed`** — 영속(즉시)과 dead-letter(백오프 소진) 둘 다 이 상태로 두고 **사유(reason/last_error)를 저장해 구분**(`conflict_blocked` 가 단일 상태인 것과 동일 패턴, 스키마 최소 변경). (4) **dead-letter 는 폐기 아님 — 운영자 재처리 가능 상태**.
- 반영 위치: FR-2, FR-3, 4. 변경 항목/영향 범위
- OQ-3: 거버넌스 경로 — 기존 REQ(REQ-017 어댑터, REQ-019 outbox, REQ-018/055 가시화, REQ-020 문서)의 change-req 로 처리할지, 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-017(LedgerPort + portfolio-hub 어댑터: 404/400/422→`LedgerPermanentError` 분류, 계약 헤더 갱신), REQ-019(ledger outbox durability: 지수 백오프+jitter+`next_retry_at`+max-attempts+단일 터미널 `delivery_failed`+status enum/컬럼 마이그레이션+`list_pending` 백오프 스킵+env 설정), REQ-018(체결→원장 fill hook: 터미널/dead-letter WARN+메트릭 가시화 — dcy-011 의 `ledger_skipped` 카운터 패턴 정합, `find_ledger_gap_slots` REQ-055 연계), REQ-020(ledger 레이어 baseline 문서 정합: runtime-flows/data-model 갱신). 백오프 env 설정은 REQ-019 에 흡수. 신규 REQ 는 원칙적으로 생성하지 않으며, dead-letter 운영자 재처리 트리거를 이번 주기에 포함하기로 하면 REQ 정제에서 소형 신규 REQ 가능(조건부).
- 반영 위치: 1. 거버넌스 해석, R-1~R-3 완화 방안, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 404 응답이 즉시 터미널 실패 상태로 전이되고 재시도가 0회 추가된다(미등록 포트폴리오 시나리오에서 attempts 폭증 없음).
- S-2: 일시적 오류(5xx 등)가 지수 백오프로 재시도되고 max-attempts 초과 시 dead-letter 로 전이된다(무한 1초 재시도 0건).
- S-3: 터미널/dead-letter 실패가 WARN/메트릭·구분 status 로 가시화되어 정상 pending 과 구별된다.
- S-4: 영속/백오프 대기 행이 `list_pending` FIFO 배치를 잠식하지 않아 신규 행이 지연 없이 처리된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 404 응답 fixture 단위 테스트에서 터미널 상태 전이·재시도 0 확인 / 데이터 출처: portfolio_hub 어댑터·outbox 테스트, ledger_outbox status/attempts
- S-2 측정 방식: 5xx 반복 fixture 에서 백오프 간격 증가·max-attempts 후 dead-letter 확인 / 데이터 출처: outbox 처리 테스트, next_retry_at/attempts/status
- S-3 측정 방식: 터미널/dead-letter 발생 시 WARN·메트릭·status 확인 / 데이터 출처: 데몬 로그, 메트릭, ledger_outbox status
- S-4 측정 방식: 영속/백오프 대기 행 다수 + 신규 행 주입 시 신규 행 처리 확인 / 데이터 출처: list_pending 결과, 처리 순서/지연
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: permanent 분류(OQ-1)·백오프 정책(OQ-2)·거버넌스(OQ-3)·dead-letter 재처리 범위 확정으로 In/Out of Scope 채워짐
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(영속 분류 과대)·R-2(스키마 마이그레이션)·R-3(누락 고착)의 change-req 대상(REQ-017/019/018/020)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap.md
- 참조 문서:
- GitHub issue #13 (fourblocks2020/trex-core) — portfolio-hub 404 영구 재시도 버그
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md — portfolio-hub ledger 경로 도입 근거
- docs/discovery/dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve.md — ledger skip 가시화·find_ledger_gap_slots 패턴(A-1)
- docs/discovery/dcy-010_20260607_safety-sell-persistent-rejection-backoff.md — 백오프/쿨다운 설계 관례(A-2, 경로 다름)
- trex_core/adapters/ledger/portfolio_hub.py:79-95, trex_core/application/ledger_outbox.py — 404 분류·백오프 부재 지점
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2/OQ-3 및 dead-letter 재처리 범위 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 영속 분류 = 404/400/422 터미널(408/429/5xx/네트워크 retryable, 401/403 보수적 retryable) / OQ-2 백오프 = 지수+jitter+`next_retry_at`·max-attempts(env)·단일 `delivery_failed` 터미널(사유 저장)·dead-letter 운영자 재처리 가능 / dead-letter **재처리 트리거(운영 명령/자동화)는 본 주기 Out of Scope, 후속 분리**(이번 주기는 재처리 가능한 상태 제공까지).
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-017/019/018/020 change-req, REQ-055 연계, 신규 REQ 원칙적 없음).
## DATA
- (비차단, REQ/delivery 단계) portfolio-hub 의 실제 4xx/5xx 응답 의미(404=영속, 429=rate-limit 등) 규약. 구현/검증 단계에서 실 응답·portfolio-hub 명세로 확정하며, OQ-1 의 404/400/422 영속 분류가 본 Discovery 의 FR/성공 기준을 충분히 정의하므로 confirm 을 차단하지 않는다.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (영속 분류=404/400/422 / 백오프=지수+jitter+next_retry_at·max-attempts env·단일 `delivery_failed` 터미널 / 거버넌스=REQ-017/019/018/020 change-req, 신규 REQ 원칙적 없음). C 유형 플레이스홀더 0개, 저장소 사실관계 일치. dead-letter 재처리 트리거는 Out of Scope(후속). DATA(portfolio-hub 오류 규약)는 REQ/delivery 단계 비차단. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 15:00 KST
@@ -1,250 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-016_20260608_operator-cli-rpc-commands
- 이슈명: 운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume (데몬 RPC 통일)
- 작성 시각(KST): 2026-06-08 15:49 KST
- 마지막 갱신 시각(KST): 2026-06-08 19:00 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-032/022/021/034/026 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-021=CHG-20260608-02, 그 외=CHG-20260608-01. pause/resume 는 direct-DB 유지. delivery: BAT-020
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: 사용자 요청(운영자 CLI 명령 추가)
- 해석:
- 현재 `trex-core` CLI 는 `pause/resume/paused` 만 제공하고 PostgreSQL `PauseStore` 에 직접 접근한다. 운영자가 universe 확인·슬롯 상태 확인·매수(enter)·매도(exit) 를 CLI 로 수행할 표면이 없어, 운영 편의와 일관성을 위해 CLI 명령을 확장한다.
- 이번 Discovery에서는 universe/슬롯 읽기·enter/exit·pause/resume 를 **데몬의 기존 RabbitMQ RPC 표면(`trex.snapshot.req`/`trex.intent.req`/`trex.control.req`)으로 통일**해 단일 writer admission 을 보존하고, enter/exit 에 운영 안전장치를 넣는 범위를 판정한다. 서버 표면은 대체로 존재하나 운영자 조회의 워치독 부수효과(REQ-034)를 막는 서버측 억제 모드가 동반된다. 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-032/022/021/034/026) `change-req` 로 처리하고 신규 REQ 는 원칙적으로 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-015) 중 운영자 CLI 의 universe/슬롯/enter/exit 명령을 다룬 문서는 없다. dcy-002(데몬+HTTP API), dcy-006(전략 transport), dcy-008(kill-switch) 등이 RPC 표면을 도입했으나 CLI 클라이언트 확장을 정의하지 않았다(REQ-032 의 CLI 는 pause/resume 만).
- 변경이 CLI 진입점(REQ-032)과 messaging transport(REQ-022) 사용을 확장하고 운영자에게 매수/매도 실행 표면을 추가하므로, 기존 Discovery 미세 조정이 아니라 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/runtime/cli.py`(전체 47줄)는 `pause`/`resume`/`paused` 3개 서브커맨드만 제공하고, `PostgreSQLPauseStore`(cli.py:32~42)에 **직접 접근**한다. universe/슬롯/매수/매도 명령은 없다.
- 데몬 RPC 표면은 이미 존재한다 — `trex_core/adapters/messaging/topology.py` 에 `SNAPSHOT_REQ_QUEUE="trex.snapshot.req"`(read-only), `INTENT_REQ_QUEUE="trex.intent.req"`(admission), `CONTROL_REQ_QUEUE="trex.control.req"`(pause/resume/kill_switch) 가 정의돼 있고, 모두 request-reply(`reply_to=amq.rabbitmq.reply-to`)다.
- 인스턴스 라우팅도 존재한다 — `_instance_queue(base, instance_key)`(topology.py:28~)는 named instance 면 `{base}.{instance_key}`, default 면 literal `base` 를 쓴다(dcy-009 다중 인스턴스).
- **주의(워치독 부수효과)**: `make_snapshot_rpc_handler`(messaging_handlers.py:196~200)는 read-only 응답 산출에 더해 **응답에 포함된 모든 holding 슬롯의 워치독 `last_seen_at` 을 갱신**한다(REQ-034). 이는 "전략 모듈 폴링 = 생존 신호"로, 폴링이 끊긴(워치독 만기) 슬롯만 safety-sell 트리거 대상이 되게 한다. 현재 이 갱신은 actor/플래그 구분 없이 모든 snapshot 요청에 적용된다.
- buy/sell 계약은 최근 Discovery 로 정리됐다 — buy 는 `portfolio_id` 필수 + symbol 서버 resolve(dcy-011, 구현 완료), sell 은 slot_id 대상 + optional 가격(없으면 시장가)(dcy-012, confirmed).
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/interface-contract.md`)는 존재하며 최신이다. CLI/운영 흐름은 runtime flow·interface-contract 에 속한다.
- 기대 상태
- 운영자가 CLI 로 universe(snapshot RPC)·슬롯 상태(snapshot RPC)·enter(buy intent RPC)·exit(sell intent RPC)·pause/resume 를 수행한다.
- 모든 명령이 데몬 RPC 를 거쳐 단일 writer admission·불변식을 보존한다(특히 enter/exit 은 DB 직접 쓰기 금지).
- enter/exit 실행 전 확인 프롬프트·live 가드 등 안전장치가 동작해 오발주를 방지한다.
- 다중 인스턴스 환경에서 CLI 가 대상 인스턴스 큐로 정확히 라우팅한다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — CLI 표면·안전장치·트랜스포트 승인
- 운영 담당자 — CLI 로 일상 운영(조회·매매·일시정지) 수행
- 전략 모듈 개발자 — 동일 RPC 표면을 공유하므로 계약 일관성 관심
- 제약사항
- enter/exit(매수/매도)는 단일 writer 인 데몬 admission(슬롯 할당·portfolio_id 검증·상태 전이)을 반드시 거쳐야 하며 DB 직접 쓰기로 우회할 수 없다.
- RPC 통일은 데몬·RabbitMQ 가 가동 중일 때만 동작한다(오프라인 조회 불가는 의도된 트레이드오프).
- 다중 인스턴스(dcy-009)에서 CLI 는 대상 인스턴스를 지정해야 큐 라우팅이 맞는다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 `trex-core universe` 로 당일 거래 대상 자산(portfolio_id/symbol)을 확인한다(snapshot RPC).
- 운영자가 `trex-core slots --status holding` 으로 현재 슬롯 상태를 조회한다(snapshot RPC).
- 운영자가 `trex-core enter <portfolio_id> --capital N` 으로 매수를 넣되, 확인 프롬프트(또는 `--yes`)와 live 가드를 거쳐 안전하게 admission 한다.
- 운영자가 `trex-core exit <slot_id> [--price P]` 로 매도를 넣는다(가격 없으면 시장가, dcy-012).
- 운영자가 `trex-core pause/resume <portfolio_id>` 로 portfolio 를 일시정지/해제한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- `trex-core` CLI 에 `universe`/`slots`/`enter`/`exit` 서브커맨드를 추가하고, 모두 **데몬 RabbitMQ RPC**(`trex.snapshot.req`/`trex.intent.req`)로 request-reply 한다(읽기도 RPC 통일 — 결정됨).
- enter/exit 에 운영 안전장치를 추가한다(OQ-2 A안) — 확인 프롬프트 기본 ON + `--yes`(paper) / `--yes --live`(live 이중 ack) + `--dry-run` 미리보기.
- 운영자 조회(`universe`/`slots`)가 워치독 생존 신호로 작동하지 않도록 snapshot RPC 에 워치독 억제 플래그(`touch_watchdog=false`/`actor="operator"`, OQ-4)를 추가한다(REQ-034 서버 부수효과 대응; 기본값=현행 유지로 하위호환).
- 전 명령에 `--json` 출력 모드를 추가한다(기계용 비대화형 — enter/exit 는 `--yes`/`--yes --live` 필수).
- CLI 에 인스턴스 라우팅 옵션을 추가해 다중 인스턴스 큐로 정확히 라우팅한다(OQ-1: `--config` canonical + `--instance` 단축, 미지정 시 `default`, 대상 인스턴스 항상 표시).
- `pause/resume` 는 기존 동작을 유지하되, 전 명령 RPC 통일 관점에서 `trex.control.req` 정합 여부를 정한다(범위 경계 참조).
- `docs/interface-contract.md`·`docs/runtime-flows.md` 에 CLI 명령·RPC 매핑·안전장치 흐름을 반영한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/runtime/cli.py` — `universe`/`slots`/`enter`/`exit` 서브커맨드 + 인스턴스 옵션 + enter/exit 가드 + RPC 클라이언트 호출
- `trex_core/adapters/messaging/`(rabbitmq.py/messaging.py) — CLI 용 request-reply RPC 클라이언트 재사용/노출(전략 모듈이 쓰는 동일 클라이언트)
- `trex_core/application/messaging_handlers.py` — `make_snapshot_rpc_handler`(196~200)에 워치독 억제 모드 추가(운영자 조회 시 `last_seen_at` 미갱신, REQ-034/021) — R-4 대응, 서버측 변경
- `trex_core/runtime/config.py` — CLI 가 RabbitMQ 접속·인스턴스 키를 읽도록 설정 활용(기존 env/TOML 재사용)
- 문서 영향:
- `docs/interface-contract.md` — CLI 명령 ↔ RPC payload 매핑(buy=portfolio_id 필수, sell=slot_id+optional price) 반영
- `docs/runtime-flows.md` — 운영자 CLI → RPC → admission 흐름·안전장치 반영
- 운영 영향:
- 운영자가 CLI 로 조회·매매·일시정지 수행 → 운영 편의 향상
- enter/exit 실주문 표면 추가 → 안전장치·권한·paper/live 운영 정책 필요
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `trex-core universe` 는 `trex.snapshot.req` RPC 로 당일 universe(자산 목록: portfolio_id/symbol 등)를 조회·출력한다.
- FR-2: `trex-core slots [--status ...]` 는 snapshot RPC 응답의 슬롯 상태(상태/심볼/수량/가격 등)를 조회·출력한다(`view=held`/holding 필터 포함).
- FR-2a: 운영자 CLI 조회(`universe`/`slots`)는 **워치독 생존 신호로 작동하지 않아야 한다** — snapshot RPC 요청에 워치독 억제 플래그(`touch_watchdog=false`/`actor="operator"`, OQ-4)를 실어, 서버 핸들러가 응답 holding 슬롯의 `last_seen_at` 갱신(REQ-034)을 건너뛴다. 기본값(플래그 미지정)은 현행 전략 폴링 갱신 유지(하위호환). CLI 조회는 항상 억제 플래그를 보낸다.
- FR-3: `trex-core enter <portfolio_id> [--capital/--qty ...]` 는 `trex.intent.req` buy 인텐트를 RPC 로 제출한다(symbol 은 서버 resolve, dcy-011). DB 직접 쓰기는 하지 않는다.
- FR-4: `trex-core exit <slot_id> [--price P]` 는 `trex.intent.req` sell 인텐트를 RPC 로 제출한다(가격 미지정 시 시장가, dcy-012). DB 직접 쓰기는 하지 않는다.
- FR-5: enter/exit 안전장치(OQ-2 A안) — 확인 프롬프트 기본 ON(대상 인스턴스·paper/live·portfolio_id/slot_id·qty/capital·가격 요약, 기본 No), `--yes` 로 생략(paper), **live 는 `--yes --live` 이중 ack**, `--dry-run` 으로 미제출 미리보기. paper/live 는 데몬 동일 env/`--config` 로 판별·표시한다.
- FR-6: 모든 RPC 명령은 대상 인스턴스 큐로 라우팅한다 — `--config <toml>`(canonical, REQ-048) 또는 `--instance <key>`(단축, override), 미지정 시 `default`(OQ-1). 대상 인스턴스를 명령 출력에 항상 표시하고, RPC 응답(admitted/rejected/reason)을 운영자에게 표시한다.
- FR-7: 전 명령에 `--json` 출력 모드를 제공한다 — 사람용 표 대신 RPC 응답을 JSON 으로 출력(스크립트/`jq` 용). `--json` 은 기계용 비대화형 모드이므로 확인 프롬프트와 양립하지 않는다: enter/exit 에 `--json` 을 쓰면 `--yes`(live 는 `--yes --live`)가 필수이며, 미지정 시 프롬프트 대신 에러로 거부한다(자동 제출 금지).
- FR-8: `docs/interface-contract.md`·`docs/runtime-flows.md` 에 CLI 명령·RPC 매핑·안전장치·출력 모드를 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 단일 writer 보존 — enter/exit 는 데몬 admission 을 거쳐 슬롯 할당·portfolio_id 검증·상태 전이 불변식이 깨지지 않아야 한다.
- NFR-2: 안전성 — enter/exit 오발주를 막는 확인/가드가 기본 동작이며, live 계좌에서는 더 엄격해야 한다.
- NFR-3: 일관성 — CLI 와 전략 모듈이 동일 RPC 계약을 사용해, 표면 분기로 인한 동작 차이가 없어야 한다.
- NFR-4: 운영성 — RPC 실패(데몬 미가동·타임아웃·라우팅 오류)를 명확한 에러로 표시해 운영자가 원인을 알 수 있어야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- `universe`/`slots`/`enter`/`exit` CLI 명령(전부 데몬 RPC) + 응답 표시
- enter/exit 안전장치(확인 프롬프트/`--yes`/live 가드)
- 운영자 조회의 워치독 억제 모드(snapshot RPC 서버 변경, REQ-034 — R-4 대응)
- 인스턴스 라우팅 옵션
- 전 명령 `--json` 출력 모드(비대화형 — enter/exit 는 `--yes` 필수)
- interface-contract/runtime-flows 문서 갱신
- Out of Scope
- 신규 서버측 RPC 표면/도메인 로직(기존 표면 재사용 — 단, 워치독 억제 모드는 snapshot 핸들러의 국소 서버 변경으로 In Scope)
- buy/sell 계약 자체 변경(dcy-011/dcy-012 에서 처리)
- kill_switch CLI 노출(별도 결정 — 본 주기는 enter/exit/pause/resume 까지)
- pause/resume 의 control RPC 통일 — 미채택(기존 direct-DB 유지). PauseStore 가 durable source 라 동작에 문제 없어 변경하지 않는다(OQ-3 결정).
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): enter/exit 가 실주문을 내는 표면이라, 오발주(잘못된 portfolio_id/slot_id/가격)나 live 오작동 시 금전 손실로 직결된다.
- 완화 방안: FR-5(OQ-2 A안)의 확인 프롬프트 기본 ON·`--yes`(paper)·`--yes --live` 이중 ack·`--dry-run` 을 기본 동작으로 하고, RPC 응답(rejected/reason)을 명확히 표시한다. admission 의 portfolio_id 검증(dcy-011)·slot 상태 검증이 1차 방어선이다.
- R-2 (Medium): 다중 인스턴스에서 CLI 가 잘못된 인스턴스 큐로 라우팅하면 의도와 다른 인스턴스에 매매가 들어간다.
- 완화 방안: (OQ-1) `--config`(canonical, 데몬과 동일 경로) 우선 + `--instance` 단축, 미지정 시 `default`. 모든 명령 출력·enter/exit 확인 프롬프트에 대상 인스턴스를 항상 표시해 오라우팅을 사전 차단한다.
- R-3 (Low): RPC 통일로 데몬·RabbitMQ 미가동 시 조회조차 불가하다(기존 pause 는 direct-DB 라 가능했음).
- 완화 방안: 의도된 트레이드오프임을 문서화하고, RPC 실패를 명확한 에러로 표시한다. 필요 시 읽기 전용 direct-DB 폴백은 후속 검토.
- R-4 (High): 운영자 CLI 조회(`universe`/`slots`)가 snapshot RPC 의 워치독 갱신 부수효과(REQ-034)를 일으키면, 전략이 실제로 죽어 있어도 운영자 조회가 생존 신호로 오인되어 **safety-sell(워치독 만기 강제청산)이 억제**된다 — 안전 기능 무력화.
- 완화 방안: FR-2a 로 운영자 조회는 워치독 억제 플래그(OQ-4)를 실어 서버가 `last_seen_at` 갱신을 건너뛴다. 스냅샷 RPC 계약/핸들러(REQ-034/021) 변경이 동반되며, 기본값은 현행(전략 폴링=워치독 갱신) 유지로 하위호환.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 서버측 RPC 표면(`trex.snapshot.req`/`trex.intent.req`/`trex.control.req`)과 request-reply 클라이언트가 이미 구현돼 있어, CLI 는 대부분 thin 확장으로 충분하다. 단, 워치독 억제(R-4/FR-2a)는 클라이언트만으로 불가하며 snapshot 핸들러(REQ-034)의 서버측 변경이 동반된다.
- A-2: dcy-011 구현 완료로 buy 는 portfolio_id 필수 + symbol 서버 resolve 이므로, `enter <portfolio_id>` 만으로 매수 제출이 가능하다.
- A-3: dcy-012 구현이 완료되면 sell 인텐트에 optional 가격 필드가 있어 `exit <slot_id> [--price]` 가 성립한다(미구현 시 exit 은 시장가 전용으로 한정 가능).
- A-4: 인스턴스 라우팅 헬퍼(`_instance_queue`)가 존재하므로 CLI 도 동일 규칙으로 큐를 계산할 수 있다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 인스턴스 라우팅 지정 방식 — `--instance <key>` 플래그, `--config <toml>`(REQ-048) 재사용, env default 중 무엇을 표준으로 하는가? default 인스턴스 처리 규칙은?
- 상태: Answered (2026-06-08, 사용자 결정 — `--config` canonical + `--instance` 단축)
- 결정: **`--config <path>.toml`(REQ-048) 를 canonical** 로 한다(데몬 기동과 동일 경로·`instance_key` 일치 보장). **`--instance <key>` 는 단축/override**(둘 다 주면 `--instance` 우선). 미지정 시 **literal `default` 인스턴스**. RabbitMQ 접속은 env(`TREX_RABBITMQ_*`) 재사용, 큐 네이밍은 `_instance_queue` 규칙. 오발주 방지를 위해 **모든 명령 출력·enter/exit 확인 프롬프트에 대상 인스턴스를 항상 표시**(R-2 완화, OQ-2 가드와 연결).
- 반영 위치: FR-6, R-2
- OQ-2: enter/exit 안전장치 구체 정책 — 확인 프롬프트 기본 on + `--yes` 생략, live 계좌 가드(예: 추가 확인/환경변수)·dry-run 제공 범위는?
- 상태: Answered (2026-06-08, 사용자 결정 — A안)
- 결정: (1) **확인 프롬프트 기본 ON** — 실행 전 대상 인스턴스·broker(paper/live)·portfolio_id/slot_id·qty/capital·가격(또는 시장가) 요약을 표시하고 `y/N`(기본 No). (2) **`--yes` 로 프롬프트 생략**(paper 자동화). (3) **live 는 이중 ack** — `--yes` 만으로 부족하고 `--yes --live`(명시적 live 승인) 를 요구해 muscle-memory 오발주 차단(데몬 `TREX_KIS_ALLOW_LIVE` 배포 게이트 위에 액션 레벨 게이트 추가). (4) **`--dry-run`** — 해석된 intent + 대상 인스턴스만 출력, 미제출. paper/live 판별은 CLI 가 데몬과 동일 env(`TREX_BROKER`/`TREX_KIS_ALLOW_LIVE`)/`--config` 로 표시.
- 반영 위치: FR-5, NFR-2, R-1
- OQ-3: 거버넌스 경로 — CLI 진입점(REQ-032)·messaging transport(REQ-022)·제어 채널(REQ-043)·**스냅샷 워치독 부수효과(REQ-034)/메시지 계약(REQ-021)**의 change-req 로 처리할지, CLI 명령을 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-032(런타임 제어 CLI: `universe`/`slots`/`enter`/`exit` 서브커맨드 + 인스턴스 라우팅 + enter/exit 안전장치), REQ-022(messaging transport: CLI request-reply RPC 클라이언트 배선), REQ-021(메시지 계약: snapshot 요청 payload 에 워치독 억제 플래그 추가), REQ-034(워치독: 핸들러가 억제 플래그 시 `last_seen_at` 미갱신), REQ-026(messaging baseline 문서 정합: interface-contract/runtime-flows CLI·RPC 매핑·플래그 반영). pause/resume 는 기존 direct-DB 유지(REQ-043 control RPC 통일 미채택). 신규 REQ 는 원칙적으로 생성하지 않으며, CLI 명령군을 독립 추적해야 한다고 판단되면 REQ 정제에서 소형 신규 REQ 가능(조건부).
- 반영 위치: 1. 거버넌스 해석(추가), R-1·R-4 완화, 8. 확인 체크
- OQ-4: 운영자 조회의 워치독 억제 메커니즘 — snapshot RPC 요청에 `actor="operator"`/`touch_watchdog=false` 플래그를 추가해 서버가 워치독 갱신을 건너뛰는 방식인가, 아니면 워치독 갱신을 요청 `strategy_id` 의 슬롯에만 적용하도록 REQ-034 동작을 좁히는 방식인가?
- 상태: Answered (2026-06-08, 사용자 결정 — 플래그 방식)
- 결정: snapshot RPC 요청에 워치독 억제 플래그(예: `touch_watchdog=false` 또는 `actor="operator"`)를 추가하고, `make_snapshot_rpc_handler` 가 그 플래그면 `watchdog_store.update(...)` 를 건너뛴다. **기본값 = 현행(플래그 미지정 시 전략 폴링이 워치독 갱신)** 으로 기존 전략 모듈 무변경(하위호환). 운영자 CLI(`universe`/`slots`)는 항상 억제 플래그를 보낸다. strategy_id-scoped 방식은 기존 REQ-034 동작을 바꿔 미채택.
- 반영 위치: FR-2a, 4. 변경 항목/영향 범위, R-4
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: `trex-core universe`·`slots` 가 데몬 snapshot RPC 응답을 정확히 조회·출력한다(직접 DB 접근 없이).
- S-2: `trex-core enter`·`exit` 가 `trex.intent.req` RPC 로 인텐트를 제출하고 admitted/rejected 응답을 표시한다(DB 직접 쓰기 0건).
- S-3: enter/exit 가 확인 프롬프트/`--yes`·live 가드를 거치며, 미확인 시 주문이 나가지 않는다.
- S-4: 지정한 인스턴스 큐로 RPC 가 라우팅되고, 잘못된 인스턴스/데몬 미가동 시 명확한 에러를 표시한다.
- S-5: `--json` 출력이 유효한 JSON 이며, `--json` + enter/exit 에 `--yes` 미지정 시 자동 제출 없이 에러로 거부된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: stub/in-memory messaging 으로 snapshot RPC 응답 주입 후 CLI 출력 검증 / 데이터 출처: CLI 테스트, snapshot RPC 응답 fixture
- S-2 측정 방식: intent RPC mock 으로 admitted/rejected 응답 시 CLI 동작·출력 검증, DB 미접근 확인 / 데이터 출처: CLI 테스트, intent RPC 응답, 슬롯 저장소 무변경
- S-3 측정 방식: 확인 프롬프트/`--yes`/live 가드 분기 테스트(미확인 시 RPC 미호출) / 데이터 출처: CLI 테스트, RPC 호출 여부
- S-4 측정 방식: 인스턴스 키별 라우팅 큐 검증 + 데몬 미가동/타임아웃 에러 표시 검증 / 데이터 출처: 라우팅 계산, RPC 타임아웃 케이스
- S-5 측정 방식: `--json` 출력 파싱(유효 JSON) + `--json` enter/exit 의 `--yes` 미지정 거부 테스트 / 데이터 출처: CLI 테스트, 출력 파서, RPC 호출 여부
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1 라우팅·OQ-2 안전장치·OQ-3 거버넌스·OQ-4 워치독 억제·pause/resume 정합(기존 유지) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-5 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(오발주/실주문)·R-4(워치독 억제)의 change-req 대상(REQ-032/022/021/034/026)이 OQ-3 으로 확정됨. 안전장치는 OQ-2(A안)로 확정. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-4 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-016_20260608_operator-cli-rpc-commands.md
- 참조 문서:
- 사용자 요청(운영자 CLI 명령 추가: universe/슬롯/enter/exit/pause/resume)
- trex_core/runtime/cli.py — 기존 CLI(pause/resume/paused, direct-DB)
- trex_core/adapters/messaging/topology.py — snapshot/intent/control RPC 큐 + 인스턴스 라우팅
- docs/discovery/dcy-011_..._buy-intent-portfolio-id-required-symbol-resolve.md — buy 계약(portfolio_id 필수, A-2)
- docs/discovery/dcy-012_..._sell-order-pricing-entry-price-bug.md — sell 계약(slot_id+optional price, A-3)
- docs/interface-contract.md — RPC payload 계약(intent/snapshot/control)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2/OQ-3/OQ-4 및 pause/resume 정합 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 인스턴스 라우팅 = `--config`(canonical, REQ-048) + `--instance`(단축 override), 미지정 시 `default`, 대상 인스턴스 항상 표시 / OQ-2 안전장치 = 확인 프롬프트 기본 ON + `--yes`(paper) / `--yes --live`(live 이중 ack) + `--dry-run` / OQ-3 거버넌스 = REQ-032/022/021/034/026 change-req(신규 REQ 없음) / OQ-4 워치독 억제 = snapshot RPC 플래그 방식(`touch_watchdog=false`/`actor`, 기본=현행 유지) / pause-resume = 기존 direct-DB 유지(control RPC 통일 미채택). 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-032/022/021/034/026 change-req, pause/resume 기존 유지, 신규 REQ 원칙적 없음).
## DATA
- 없음
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 트랜스포트(전부 RPC)·enter/exit 안전장치·`--json` 모드 반영. 운영자 조회의 워치독 부수효과(R-4) 발견·억제 반영. 오픈 질문 OQ-1~OQ-4 모두 Answered (인스턴스 라우팅=`--config`+`--instance` / 안전장치=confirm+`--yes`/`--yes --live`+`--dry-run` / 거버넌스=REQ-032/022/021/034/026 change-req / 워치독 억제=플래그). pause/resume 기존 유지. C 유형 플레이스홀더 0개, 저장소 사실관계 일치. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 17:00 KST
@@ -1,229 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-017_20260608_watchdog-ownership-visibility
- 이슈명: 전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화 (운영 모니터링)
- 작성 시각(KST): 2026-06-08 16:33 KST
- 마지막 갱신 시각(KST): 2026-06-08 20:00 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-034/021/037/038/039/032/042 (전부 change-req, 신규 REQ 없음 — OQ-3). REQ-034=CHG-20260608-02, REQ-021=CHG-20260608-03, REQ-032=CHG-20260608-02, REQ-037/038/039/042=CHG-20260608-01. Redis 미채택. delivery: BAT-021
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #12 (fourblocks2020/trex-core), 관련 issue #11(dcy-011, ledger silent skip), dcy-016(운영자 CLI)
- 해석:
- 슬롯이 내부 청산 루틴(safety-sell 워치독) 소유로 넘어가는 것 = 그 슬롯을 책임지던 전략 모듈이 heartbeat(스냅샷 RPC)를 멈췄다는 신호다. 그런데 이 상태가 완전히 무성(silent)이라 운영자가 "전략이 죽어 슬롯이 안전망으로 넘어갔다"는 가장 중요한 경고를 인지할 방법이 없다. 이를 가시화한다.
- 이번 Discovery에서는 **가시화 메커니즘**을 판정한다 — issue #12 는 Redis projection(TTL+pub/sub)을 제안하나, dcy-016 으로 운영자 조회가 워치독을 리셋하던 함정이 이미 해소되었으므로, **CLI `slots`(스냅샷 RPC 에 워치독 상태 노출) + safety-sell 이벤트 로깅** 으로 Redis 없이 관측을 충족한다(OQ-1 (a)). 거버넌스는 OQ-3 결정에 따라 기존 REQ(REQ-034/021/037/038/039/032/042) `change-req` 로 처리하고 신규 REQ 는 생성하지 않는다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~dcy-016) 중 워치독 소유/전략 이탈 상태의 운영 가시화를 다룬 문서는 없다. dcy-007(safety-sell)은 트리거 동작을, dcy-016(운영자 CLI)은 조회/매매 표면을 도입했으나 워치독 소유 상태 노출·알림을 정의하지 않았다.
- 변경이 워치독 상태 노출(REQ-034)·safety-sell 가시화(REQ-037) 의미를 확장하고(필요 시 Redis projection 신규 도입), 운영 모니터링 표면을 추가하므로 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/application/watchdog.py` 의 `WatchdogStore` 는 **순수 in-memory dict**(`last_seen` + `is_expired(slot_id, timeout_sec)`)다. DB 테이블도 조회용 RPC 도 없고, 데몬 재시작 시 소실된다. 만기 기준은 `watchdog_timeout_sec`(기본 120s, config.py).
- `trex_core/application/safety_sell.py` 의 `check_forced_close`/`check_prices`/`check_market_close`(143~)는 발동 슬롯 리스트(`fired`)를 반환하지만 `run_once` 가 이를 영구화·로깅하지 않고 버린다. safety_sell 로그는 WARN 3개(slot sell 실패·KIS 쿼터·market-close 실패)뿐이고 평가/발동/스킵에는 로그가 없다 → 무성.
- 스냅샷 RPC 응답은 슬롯 status/symbol/qty/price 만 운반하고 **워치독 상태(expired/last_seen)를 노출하지 않는다**(`make_snapshot_rpc_handler`, messaging_handlers.py:163~). 즉 현재는 CLI 로도 "어느 슬롯이 워치독 소유인지" 알 수 없다.
- **(중요) dcy-016 으로 "관측이 워치독을 리셋"하던 함정은 이미 해소됐다** — `make_snapshot_rpc_handler` 가 `touch_watchdog=false`/`actor="operator"` 요청이면 `last_seen` 을 갱신하지 않는다(messaging_handlers.py:201~207). 즉 issue #12 가 든 "유일한 조회 수단이 워치독을 리셋"한다는 전제는 더 이상 유효하지 않다.
- Redis 는 이미 의존성이다 — `redis>=5`, `RedisTokenCache`(best-effort, 오류 시 no-op), `TREX_KIS_REDIS_URL`. 새 인프라 도입이 아니라 기존 패턴 재사용 가능.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신이다. 워치독·safety-sell·CLI 는 runtime flow 에 속한다.
- 기대 상태
- 운영자가 "전략 이탈 → 슬롯이 워치독 소유로 넘어감(=안전망 발동 대상)" 을 실시간으로 인지한다.
- 관측 행위가 워치독을 리셋하지 않는다(dcy-016 으로 이미 충족).
- safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·전략 복귀 이탈이 로그/메트릭(또는 알림)으로 구분 가시화된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 가시화 메커니즘(CLI/RPC vs Redis)·거버넌스 승인
- 운영 담당자 — 전략 이탈/안전망 발동을 실시간 모니터링·대응
- 대시보드/알림 연동 — (Redis 채택 시) projection/pub-sub 소비
- 제약사항
- **Best-effort 원칙**: 가시화 부수효과(Redis/로그/메트릭)는 매매·청산을 절대 멈추면 안 된다. 워치독 판정의 source of truth 는 기존 in-memory 경로를 유지한다.
- 다중 인스턴스(dcy-009) 네임스페이스 — 키/표시에 instance 포함.
- WatchdogStore 가 in-memory 라 재시작 시 상태가 소실된다(메커니즘 선택에 영향).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략 모듈이 죽어 스냅샷 폴링이 끊긴다 → 해당 holding 슬롯의 워치독이 만기되어 safety-sell 소유로 넘어간다 → 운영자가 `trex-core slots`(또는 알림)로 "이 슬롯이 워치독 소유(전략 이탈)" 임을 즉시 인지한다.
- safety-sell 이 forced_close/stop_loss 로 슬롯을 실제 청산한다 → 발동 사실·사유·슬롯이 로그/메트릭(또는 알림)으로 남아 사후 추적된다.
- 전략이 복귀해 폴링을 재개한다 → 워치독 소유에서 이탈한 것이 가시화되어 경고가 stuck 되지 않는다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- (OQ-1 (a)) 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired/last_seen) 필드를 추가하고 `trex-core slots` 가 "워치독 소유(전략 이탈)" 를 표시한다(워치독 억제 읽기 = dcy-016 으로 이미 비침습).
- safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·이탈을 로그/메트릭으로 가시화한다(현재 무성 → 가시화, dcy-011/#11 패턴 정합).
- baseline 문서에 워치독 가시화 흐름·스냅샷 응답 워치독 필드를 반영한다.
- (미채택) Redis projection·pub/sub 알림은 본 주기에 도입하지 않는다 — push 알림이 후일 하드 요구가 되면 후속 분리.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/safety_sell.py` — `run_once`/`check_*` 가 발동·소유·스킵을 로그/메트릭으로 가시화(현재 버려지는 `fired` 리스트 활용)
- `trex_core/application/messaging_handlers.py` — 스냅샷 응답에 워치독 expired/last_seen 필드 추가(`WatchdogStore` + `watchdog_timeout_sec` 로 슬롯별 계산)
- `trex_core/runtime/cli.py` — `slots` 가 워치독 상태(소유/전략 이탈) 컬럼 표시(dcy-016 의 `slots` 확장)
- 문서 영향:
- `docs/runtime-flows.md` — 워치독 소유·safety-sell 발동 가시화 흐름 반영
- `docs/interface-contract.md` — 스냅샷 응답 워치독 상태 필드 계약 반영
- 운영 영향:
- 전략 이탈/안전망 발동을 CLI 조회·로그로 인지 → 운영 대응성 향상
- (push 알림·대시보드 연동은 본 주기 밖 — Redis projection 후속)
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: safety-sell 의 워치독 소유·발동을 phase별 차등 레벨로 가시화한다(OQ-2 A안) — WARN: 워치독 소유 진입(전략 이탈)·forced_close·stop_loss / INFO: take_profit·market_close·워치독 이탈(복귀) / 전 phase 카운터(phase·reason·instance_key 라벨) / per-tick 스킵·no-fire 는 카운터만(로그 X). 모든 로그·메트릭은 `slot_id`·`portfolio_id`·`reason`·`phase`·`instance_key` 구조화 필드를 포함한다. 소유 진입·이탈을 모두 남겨 경고가 stuck 되지 않는다.
- FR-2: 운영자가 "어느 슬롯이 워치독 소유(전략 이탈)인지" 를 관측할 수 있다. 관측 행위는 워치독을 리셋하지 않는다(dcy-016 의 `touch_watchdog=false` 억제 읽기 재사용).
- FR-3: (OQ-1 (a)) 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired 여부·last_seen)를 read-only 필드로 추가하고, `trex-core slots` 가 "워치독 소유(전략 이탈)" 를 표시한다. 데몬은 `WatchdogStore` + `watchdog_timeout_sec` 로 슬롯별 expired 를 계산해 응답에 싣는다. Redis projection·pub/sub 알림은 도입하지 않는다(미채택).
- FR-4: 가시화 부수효과는 best-effort 다 — Redis/로그/메트릭 오류가 매매·청산을 멈추지 않으며, 워치독 판정 source of truth 는 기존 in-memory 경로를 유지한다.
- FR-5: 다중 인스턴스 환경에서 워치독 상태 표시·로그에 instance_key 를 포함해 인스턴스 간 혼동이 없게 한다(dcy-016 의 대상 인스턴스 표시와 정합).
- FR-6: `docs/runtime-flows.md` 와 `docs/interface-contract.md`(스냅샷 응답 워치독 상태 필드)에 워치독 가시화 흐름·계약을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 안전 격리 — 가시화는 best-effort projection 이며, 어떤 실패도 워치독 판정·safety-sell 청산을 차단하지 않아야 한다.
- NFR-2: 비침습 관측 — 운영자의 관측이 워치독 `last_seen` 을 갱신해 강제청산을 지연시키지 않아야 한다(dcy-016 억제로 충족).
- NFR-3: 가시성 — 전략 이탈/안전망 발동이라는 결과 기반 신호가 무성으로 사라지지 않고 로그/메트릭/(알림)으로 관측 가능해야 한다(issue #11/dcy-011 패턴 정합).
- NFR-4: 멀티 인스턴스 정합 — 인스턴스별 상태가 키/표시에서 혼동 없이 구분되어야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- safety-sell 워치독 소유·발동·이탈의 로그/메트릭 가시화(FR-1, cheap·필수)
- 워치독 소유 상태의 운영자 관측(FR-2) — 비침습(dcy-016)
- (OQ-1 (a)) 스냅샷 RPC 응답 워치독 상태 필드 + `trex-core slots` 표시
- runtime-flows·interface-contract 문서 갱신
- Out of Scope
- 워치독 판정 로직·timeout 기준 변경(가시화만, 판정은 기존 유지)
- WatchdogStore 를 in-memory 에서 영속 store 로 승격(정본 변경) — 본 주기는 관측 노출까지
- Redis projection·pub/sub push 알림(OQ-1 (a) 로 미채택) — push 알림이 후일 하드 요구가 되면 후속 분리
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 가시화 부수효과(특히 Redis)가 실패·지연하면 워치독 판정·safety-sell 청산을 막아 안전 기능을 해칠 수 있다.
- 완화 방안: FR-4 best-effort 격리 — `RedisTokenCache` 처럼 오류를 no-op 무시하고, source of truth 는 in-memory 유지. 가시화는 비동기·예외 격리.
- R-2 (Low, OQ-1 결정으로 해소): 메커니즘 과잉설계 위험.
- 완화 방안: OQ-1 (a)로 Redis projection 을 미채택하고 CLI/RPC 노출 + safety-sell 로깅으로 한정했다. dcy-016 으로 관측 함정이 이미 해소되어 on-demand 관측으로 issue #12 의 "무성 → 가시화" 를 충족한다. push 알림은 실제 요구 발생 시 후속.
- R-3 (Low): WatchdogStore 가 in-memory 라 재시작 시 소유 상태가 소실되어, 재시작 직후 CLI 조회가 비어 보일 수 있다(폴링 재개 시 다시 채워짐).
- 완화 방안: 가시화는 live 상태 반영임을 문서화한다. WatchdogStore 영속 승격은 Out of Scope(별도 주기). safety-sell 발동 로그(FR-1)는 재시작과 무관하게 이력으로 남는다.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-016 구현 완료로 스냅샷 RPC 에 워치독 억제 읽기(`touch_watchdog=false`/`actor="operator"`)와 `trex-core slots` 가 존재하므로, 워치독 상태 노출은 기존 표면 확장으로 충분하다(관측이 워치독을 리셋하지 않음).
- A-2: `safety_sell.check_*` 가 이미 발동 슬롯 리스트를 산출하므로, 로그/메트릭 가시화(FR-1)는 버려지던 결과를 활용하는 cheap 한 변경이다.
- A-3: Redis 가 이미 의존성이고 `RedisTokenCache` best-effort 패턴이 있어, Redis projection 채택 시 새 인프라 없이 동일 패턴을 재사용한다.
- A-4: 워치독 판정의 source of truth 는 in-memory 경로이며, 본 변경은 그 위의 관측 projection 만 추가한다(판정 동작 무변경).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 가시화 메커니즘 — (a) 스냅샷 RPC 워치독 상태 노출 + `trex-core slots` 표시(Redis 불요, pull+로그) / (b) best-effort Redis projection(TTL 연속 키 + warn/fire 이산 키·pub/sub, push 알림·대시보드) / (c) 단계적((a) 먼저, push 알림 필요 시 (b)) 중 무엇인가?
- 상태: Answered (2026-06-08, 사용자 결정 — (a) CLI/RPC 노출)
- 결정: 스냅샷 RPC 응답에 holding 슬롯의 워치독 상태(expired/last_seen) 필드를 추가하고 `trex-core slots` 가 표시한다. 관측은 dcy-016 의 `touch_watchdog=false` 억제 읽기로 비침습이다. **Redis projection·pub/sub 알림은 미채택**(이번 주기 도입 안 함). safety-sell 발동·소유·이탈 로그/메트릭(FR-1)은 메커니즘과 독립적으로 유지한다. push 알림이 후일 하드 요구가 되면 Redis projection 을 후속 분리한다.
- 반영 위치: FR-3, 범위 경계(Out of Scope: Redis projection), R-2, DATA
- OQ-2: safety-sell 가시화(FR-1)의 범위 — 어떤 phase(소유 진입/forced_close/stop_loss/take_profit/market_close/스킵/이탈)를 어떤 레벨(WARN/INFO/메트릭)로 남길지, 메트릭 키 규약은?
- 상태: Answered (2026-06-08, 사용자 결정 — A안 차등 매트릭스)
- 결정: **WARN** = 워치독 소유 진입(전략 이탈)·forced_close 발동·stop_loss 발동(주의 사건). **INFO** = take_profit 발동·market_close 발동·워치독 이탈(복귀). **메트릭(카운터)** = 전 phase(발동·소유 진입/이탈·스킵), phase/reason 라벨 + instance_key 포함. **per-tick 스킵/no-fire 평가는 로그하지 않고 카운터만**(30초 주기 로그 스팸 방지; 쿨다운 영속 거부 경고는 dcy-010/REQ-052 가 별도 처리). 모든 로그/메트릭에 `slot_id`·`portfolio_id`·`reason`·`phase`·`instance_key` 구조화 필드. dcy-011(#11 `ledger_skipped`)·dcy-013 의 WARN+카운터 패턴과 정합.
- 반영 위치: FR-1, NFR-3
- OQ-3: 거버넌스 경로 — 워치독(REQ-034)·safety-sell(REQ-037)·설정(REQ-041)·문서(REQ-042)의 change-req 로 처리할지, (Redis projection 채택 시) 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — change-req 우선)
- 결정: 기존 Implemented REQ 본문 수정(change-req)으로 처리한다. change-req 대상 — REQ-034(per-slot 워치독: 워치독 상태 expired/last_seen 노출), REQ-021(메시지 계약: 스냅샷 응답에 워치독 상태 필드), REQ-037(forced_close 자동 매도 루프: forced_close·워치독 소유 진입/이탈 가시화), REQ-038(stop-loss·take-profit 트리거: 발동 가시화), REQ-039(장 마감 청산: market_close 가시화), REQ-032(CLI: `slots` 가 워치독 상태 표시 — dcy-016 확장), REQ-042(safety-sell baseline 문서 정합: runtime-flows/interface-contract 갱신). OQ-1 (a) 로 Redis projection 을 미채택했으므로 신규 REQ 는 생성하지 않는다(메트릭은 in-process 카운터로 기존 REQ 범위 내).
- 반영 위치: 1. 거버넌스 해석, R-1 완화, 8. 확인 체크
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 전략 폴링이 끊겨 슬롯이 워치독 소유로 넘어가면, 운영자가 (CLI 조회 또는 알림으로) 그 사실·해당 슬롯을 인지할 수 있다.
- S-2: safety-sell 의 워치독 소유 접수·발동(forced_close/stop_loss/take_profit/market_close)·이탈이 로그/메트릭으로 구분 관측된다(무성 0건).
- S-3: 운영자의 워치독 상태 관측이 `last_seen` 을 갱신하지 않아 강제청산을 지연시키지 않는다(dcy-016 억제 검증).
- S-4: 가시화 부수효과(Redis/로그/메트릭) 실패를 강제 주입해도 워치독 판정·safety-sell 청산이 정상 동작한다(best-effort 격리).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 폴링 중단 → 워치독 만기 시나리오에서 CLI 조회/알림에 소유 상태 표시 확인 / 데이터 출처: `slots` 출력 또는 projection, 워치독 상태
- S-2 측정 방식: safety-sell 발동 시나리오에서 phase별 로그/메트릭 발생 확인 / 데이터 출처: 데몬 로그, 메트릭, `check_*` 결과
- S-3 측정 방식: 운영자 모드(`touch_watchdog=false`) 조회 후 `last_seen` 미갱신 확인 / 데이터 출처: WatchdogStore 상태, 스냅샷 핸들러 테스트
- S-4 측정 방식: Redis/로깅 실패 주입 후 워치독 판정·청산 정상 동작 확인 / 데이터 출처: 장애 주입 테스트, safety-sell 동작 로그
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 가시화 메커니즘(OQ-1 (a))·safety-sell 가시화 범위(OQ-2 차등 매트릭스)·거버넌스(OQ-3) 모두 확정됨
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(가시화가 안전 기능 차단)의 best-effort 격리 방식과 change-req 대상(REQ-034/021/037/038/039/032/042)이 OQ-3 으로 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-3 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin Ko (사람 승인자 지정 완료)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-017_20260608_watchdog-ownership-visibility.md
- 참조 문서:
- GitHub issue #12 (fourblocks2020/trex-core) — 워치독 소유 상태 Redis 가시화 제안
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md — safety-sell·워치독 도입 근거
- docs/discovery/dcy-016_20260608_operator-cli-rpc-commands.md — `slots` CLI·워치독 억제 읽기(A-1, CLI 대안의 기반)
- docs/discovery/dcy-011_..._buy-intent-portfolio-id-required-symbol-resolve.md — 무성 가시화 패턴(#11 계열)
- trex_core/application/watchdog.py, safety_sell.py, messaging_handlers.py — 워치독 in-memory·발동 미로깅·억제 읽기 지점
- trex_core/adapters/broker/token_cache.py — Redis best-effort 패턴(A-3)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (없음 — OQ-1/OQ-2 확정으로 해소)
> 결정 완료(2026-06-08): OQ-1 가시화 메커니즘 = (a) 스냅샷 RPC 워치독 상태 노출 + `trex-core slots`(Redis 미채택, push 알림 후속) / OQ-2 safety-sell 가시화 = 차등 매트릭스(WARN: 소유진입·forced_close·stop_loss / INFO: take_profit·market_close·이탈 / 전 phase 카운터 / per-tick 스킵=카운터만). 결정 항목에서 제외됨.
## CONFIRM
- 없음 — 거버넌스 경로는 OQ-3 으로 확정됨(REQ-034/021/037/038/039/032/042 change-req, 신규 REQ 없음).
## DATA
- (참고) push 알림(전략 이탈 즉시 통지)은 본 주기 미채택(OQ-1 (a)). 후일 push 알림이 하드 요구로 확인되면 Redis projection 후속 분리.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 오픈 질문 OQ-1~OQ-3 모두 Answered (가시화=(a) 스냅샷 RPC 워치독 노출 + `trex-core slots`, Redis 미채택 / safety-sell 가시화=차등 매트릭스 / 거버넌스=REQ-034/021/037/038/039/032/042 change-req, 신규 REQ 없음). 핵심: dcy-016 으로 관측 함정이 이미 해소되어 Redis 없이 충족. C 유형 플레이스홀더 0개, 저장소 사실관계 일치. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 17:10 KST
@@ -1,233 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-018_20260608_ledger-response-cash-position-writeback
- 이슈명: ledger write 응답의 cash/quantity 를 portfolio 스냅샷에 write-back — option D 활성화 + 수량 source 통일
- 작성 시각(KST): 2026-06-08 20:08 KST
- 마지막 갱신 시각(KST): 2026-06-08 21:45 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-017/019/029 change-req + REQ-030 Approved→Implemented (전부 change-req, 신규 REQ 없음 — OQ-3 a). REQ-017/019=CHG-20260608-02, REQ-029/030=CHG-20260608-01. delivery: BAT-023
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: 운영 관측(매수 후 universe `asset_quantity` 는 1로 바뀌나 `cash_balance` 는 불변), portfolio-hub#8(ledger 응답에 cash/quantity 반환) 요청
- 해석:
- 의도된 설계는 "ledger write → portfolio-hub 응답(cash/quantity) → trex-core SnapshotStore 갱신"(REQ-030 option D)이다. 그런데 trex-core 측에서 응답 파싱·write-back 배선이 누락되어, 현금은 일일 스냅샷 전까지 반영되지 않고 수량만 슬롯 오버레이로 살아있어 보이는 비대칭이 발생한다.
- 이번 Discovery에서는 (1) ledger write 응답의 cash/quantity 를 파싱·운반, (2) SnapshotStore write-back 배선(option D 활성화), (3) 수량 source 를 portfolio-hub 권위값으로 통일(슬롯 오버레이 정합)하는 범위를 판정한다. portfolio-hub#8 응답 계약 의존과 거버넌스도 함께 정한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- REQ-030(option D 현금/포지션 갱신)은 `Approved` 상태로 "wired but no-op until portfolio-hub#8 ships" 로 연기되어 구현되지 않았다. 본 Discovery 는 그 연기된 경로를 실제 활성화하고, ledger 응답 파싱(REQ-017)·write-back 배선(REQ-019)·수량 source 통일(REQ-029)을 함께 다룬다.
- dcy-005(snapshot read)는 option D 를 정의했으나 portfolio-hub#8 대기로 미구현 상태로 남겼다. 운영 환류로 갭이 확인되어 독립 주기로 활성화·정합하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **(1) ledger 응답 폐기**: `trex_core/adapters/ledger/portfolio_hub.py` 의 `record_trade`(portfolio_hub.py:78~94)는 2xx 응답에서 `resp.json()` 을 읽지만 **오직 `duplicate` 플래그만** 보고 `LedgerWriteResult.NEW`/`DUPLICATE_OK` 만 반환한다. portfolio-hub 가 cash/quantity 를 응답에 실어도 추출·반환하지 않는다.
- **(2) write-back 배선 부재**: `trex_core/application/ledger_outbox.py` 의 `LedgerWriter`(ledger_outbox.py:132~)는 `SnapshotStore` 참조가 없어, 응답에 값이 있어도 갱신할 경로가 없다.
- **(3) sink 만 존재, 호출자 없음**: `trex_core/application/portfolio.py` 의 `SnapshotStore.update_cash_position`(portfolio.py:148~)은 cash_balance·asset_quantity 를 쓰는 메서드로 구현돼 있으나, ledger 경로에서 호출되지 않는다(snapshot_scheduler 의 코멘트만 참조).
- **수량 비대칭(슬롯 오버레이)**: `build_snapshot_envelope`(portfolio.py:426~427)는 universe 의 `asset_quantity` 를 **로컬 슬롯 holding qty 로 override**(REQ-029)한다. 그래서 매수 직후 수량은 슬롯에서 살아있게 보이지만 portfolio-hub 권위값이 아니고, 현금은 오버레이가 없어 옛 스냅샷 값 그대로다.
- **option D 미구현**: REQ-030(option D 현금/포지션 갱신)은 `Approved`(미구현). `snapshot_scheduler.py:5~6` 주석 — "wired through `SnapshotStore.update_cash_position` but disabled at the ledger layer until portfolio-hub#8 ships".
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`)는 존재하며 최신이다. ledger 기록·스냅샷 read 는 기존 runtime flow 에 속한다.
- 기대 상태
- ledger NEW write 후 portfolio-hub 응답의 **cash/quantity 가 SnapshotStore 에 write-back** 되어, 매수/매도 직후 `cash_balance` 와 `asset_quantity` 가 모두 portfolio-hub 권위값으로 갱신된다.
- 수량과 현금이 **동일 source(portfolio-hub)** 로 통일되어, "수량은 살아있고 현금은 stale" 한 비대칭이 사라진다.
- portfolio-hub 불가/응답 누락 시에도 best-effort 로 degrade 하며 ledger 기록·청산 동작을 막지 않는다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — ledger 응답 계약·write-back·수량 source 변경 승인
- 운영 담당자 — 실시간 현금/포지션 정합 관측
- portfolio-hub 팀 — ledger write 응답에 cash/quantity 반환(portfolio-hub#8) 제공 측
- 제약사항
- ledger write 응답의 cash/quantity 필드 계약은 portfolio-hub#8 배포·응답 스키마에 의존한다(외부 의존).
- write-back 부수효과가 ledger 기록 멱등(REQ-019)·청산 동작을 깨지 않아야 한다(best-effort 격리).
- 수량 source 변경(슬롯 오버레이 → portfolio-hub)은 매수~ledger 반영 사이의 타이밍 공백 처리가 필요하다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 `trex-core enter` 로 매수 후 `trex-core universe` 를 보면, `asset_quantity` 와 `cash_balance` 가 **모두** portfolio-hub 권위값으로 갱신되어 있다(현재는 수량만).
- 매도 후에도 동일하게 현금 증가·수량 감소가 즉시 스냅샷에 반영된다.
- portfolio-hub 가 일시 불가하면 cash/quantity 가 stale 임을 degraded 로 신호하고, ledger 기록은 outbox 로 계속 진행된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- ledger 어댑터 `record_trade` 가 portfolio-hub 응답의 cash/quantity 를 파싱·운반하도록 결과 타입을 확장한다(현재 `LedgerWriteResult` 만 → cash/quantity 동반).
- `LedgerWriter` 에 `SnapshotStore` 를 주입하고, `new`(201)·`duplicate`(200) 응답의 `cash_balance_after`/`asset_quantity_after`(절대 권위값) 로 `update_cash_position(asset_id, cash, quantity)` write-back 한다(option D 활성화, best-effort; 409/422 는 `*_after` 미정의라 skip — OQ-2 b).
- 수량/현금 source 정합(OQ-1 B): **슬롯 오버레이(REQ-029)는 유지**(lifecycle 중인 슬롯은 코어 권위 즉시값, 불변식 A), write-back 은 **현금 + 슬롯 밖(미보유) base 수량**을 portfolio-hub 권위값으로 신선화. 비대칭(수량 live·현금 stale) 제거.
- 체결~write-back 공백 동안 **현금을 추정 오버레이로 즉시 표시**(OQ-4 (i)): base 권위 현금 ± 미정산 체결 cash_delta(체결가×수량 ∓ 수수료/세금), **"추정/미정산" 라벨 필수**, write-back 권위값 도착 시 교체.
- 응답 계약·write-back·수량/현금 정합을 baseline 문서(interface-contract/runtime-flows)에 반영한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/adapters/ledger/portfolio_hub.py` — `record_trade` 응답 파싱(cash/quantity) + 결과 타입 확장
- `trex_core/application/ledger_outbox.py` — `LedgerWriter` 에 `SnapshotStore` 주입 + NEW write 후 `update_cash_position` 호출(best-effort)
- `trex_core/application/portfolio.py` — `update_cash_position` 활용, 수량 source 정합(`build_snapshot_envelope` 의 슬롯 오버레이 처리)
- `trex_core/runtime/daemon.py` — LedgerWriter 에 SnapshotStore 배선
- 문서 영향:
- `docs/interface-contract.md` — ledger write 응답 계약(cash/quantity) + 스냅샷 cash/quantity source 반영
- `docs/runtime-flows.md` — ledger write → 응답 → SnapshotStore write-back 흐름 추가
- 운영 영향:
- 매매 직후 실시간 현금/포지션 정합 → 운영 관측 정확도 향상
- portfolio-hub#8 응답 계약 의존 — 배포 조율 필요
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `record_trade` 는 portfolio-hub ledger write 응답 root 의 flat 필드 **`cash_balance_after`/`asset_quantity_after`**(절대 권위값)를 파싱해 운반한다(결과 타입 확장, `new`/`duplicate` 구분 유지). status별: `new`(201)·`duplicate`(200) = 권위값 있음, 409·422 = `*_after` 미정의(dcy-005 A-7 / portfolio-hub#8 확정·동작 중).
- FR-2: `LedgerWriter` 는 `new`·`duplicate` write 성공 시 `SnapshotStore.update_cash_position(asset_id, cash_balance_after, asset_quantity_after)` 로 write-back 한다(OQ-2 b — `*_after`가 delta 아닌 절대 권위값이라 덮어써도 멱등). 409/422 는 skip. 부수효과는 best-effort — 실패가 ledger 기록 멱등(REQ-019)·청산을 막지 않는다(write 성공 *이후* 분리 수행, 예외 무시).
- FR-3: 수량 source 정합(OQ-1 B) — 슬롯 오버레이(REQ-029)는 **유지**(lifecycle 중 슬롯은 코어 권위 즉시값, 불변식 A). write-back 은 **현금 + 슬롯 밖 base 수량**을 portfolio-hub 권위값으로 신선화. 슬롯 권위와 write-back 권위가 어긋나면 숨기지 않고 포지션 정합 경고(REQ-031)로 노출.
- FR-3a: 임시 현금 추정 오버레이(OQ-4 (i)) — 체결~write-back 공백 동안 `base 권위 현금 ± 미정산 체결 cash_delta`(체결가×수량 ∓ 수수료/세금)를 **"추정/미정산" 라벨과 함께** 노출하고, write-back 권위값 도착 시 교체·미정산 delta 제거. 코어 수수료(`calculate_fee`)는 근사라 추정 오차는 라벨로 커버.
- FR-4: portfolio-hub 불가·응답 누락(또는 dead-letter, dcy-015) 시 권위 현금/수량을 갱신하지 못하므로 추정·pending 유지 + degraded(REQ-028)로 신호하고 write-back 을 건너뛴다(best-effort).
- FR-5: `docs/interface-contract.md`·`docs/runtime-flows.md` 에 응답 계약(`*_after`)·write-back 흐름·수량/현금 source·추정 오버레이를 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 안전 격리 — write-back 부수효과는 best-effort 이며, 어떤 실패도 ledger 기록 멱등(REQ-019)·청산 동작을 차단하지 않아야 한다.
- NFR-2: 권위 일관성 — cash 와 quantity 가 동일 source(portfolio-hub)에서 와 비대칭(수량 live·현금 stale)이 재발하지 않아야 한다.
- NFR-3: 멱등 정합 — DUPLICATE write 응답으로 인한 cash/quantity 중복 적용이 없어야 한다(멱등 write-back).
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- ledger write 응답 `cash_balance_after`/`asset_quantity_after` 파싱(`record_trade`) + write-back 배선(`LedgerWriter`→`update_cash_position`, `new`+`duplicate`)
- 수량/현금 source 정합(슬롯 오버레이 유지 + write-back 권위 신선화, OQ-1 B)
- 임시 현금 추정 오버레이(라벨, OQ-4 (i)) + write-back 권위 수렴
- degraded/best-effort 처리 + 문서 갱신 + REQ-030 Approved→Implemented
- Out of Scope
- portfolio-hub#8 자체(외부 — 이미 배포·동작 중, 본 주기는 trex-core 소비 측)
- 일일 스냅샷 스케줄러 자체 재설계(REQ-030 의 fetch 부분은 유지)
- 슬롯 오버레이 제거(OQ-1 B 로 **유지** 확정 — 제거 안 함)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): write-back 부수효과가 실패·지연하면 ledger 기록 루프·청산을 막을 수 있다.
- 완화 방안: FR-2 best-effort 격리(`RedisTokenCache`/dcy-017 패턴) — 예외를 no-op 무시, ledger 멱등·청산 동작 불변. write-back 은 write 성공 *이후* 분리 수행.
- R-2 (해소): portfolio-hub#8 응답 스키마 미확정 위험.
- 해소: 응답 스키마 = root flat `cash_balance_after`/`asset_quantity_after`(dcy-005 A-7 확정) + portfolio-hub#8 배포·동작 중(2026-06-08). 필드 부재(409/422) 시 write-back skip(보수). 잔여 위험 없음.
- R-3 (Low, OQ-1 B + OQ-4 로 완화): 매수~write-back 공백에 현금이 잠깐 stale 할 수 있다.
- 완화 방안: 수량은 슬롯 오버레이(정확값) 유지, 현금은 추정 오버레이(OQ-4 i, "추정/미정산" 라벨)로 즉시 표시 후 권위값 수렴. 공백 구간은 outbox drain(~1s)으로 짧다.
- R-4 (Medium): DUPLICATE write 응답에 cash/quantity 가 와도 중복 적용하면 정합이 깨진다.
- 완화 방안: NEW write 에만 write-back 적용(또는 멱등 키 기준), DUPLICATE 는 스킵(NFR-3).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: portfolio-hub 가 ledger write 응답에 갱신된 cash/quantity 를 반환한다(portfolio-hub#8). 미배포 시 본 변경은 write-back 스킵(stale)으로 안전 동작하며, 배포 후 활성화된다.
- A-2: `SnapshotStore.update_cash_position` 이 이미 구현돼 있어 sink 는 재사용 가능하고, 누락된 것은 source(응답 파싱)와 배선(writer→store)뿐이다.
- A-3: ledger 기록 멱등(correlation_id, REQ-019)은 그대로 유지되며, write-back 은 그 위의 관측 갱신이다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 수량 source 통일 방식 — 슬롯 오버레이(REQ-029)를 제거하고 portfolio-hub write-back 값으로 일원화하는가, 아니면 슬롯 오버레이를 유지하고 write-back 으로 수렴하는가?
- 상태: Answered (2026-06-08, 사용자 결정 — B: 슬롯 오버레이 유지 + write-back 수렴)
- 결정: 슬롯 오버레이(REQ-029)는 **유지**한다 — lifecycle 중인 슬롯은 코어 자체 권위·즉시·외부 무관(불변식 A). portfolio-hub write-back 은 **현금과 슬롯 밖(미보유) base 수량**을 권위값으로 신선화해 비대칭(수량 live·현금 stale)을 제거한다. (A)는 매수 직후 공백에 수량을 옛값으로 후퇴시키고 portfolio-hub 불가 시 보유 수량이 0 으로 보이는 안전 리스크가 있어 미채택.
- 반영 위치: FR-3, 4. 변경 항목, 범위 경계
- OQ-2: write-back 시점/실패 처리 — 어떤 응답에 write-back 하고, 불가·DUPLICATE·잔고부족 시 규칙은?
- 상태: Answered (2026-06-08, 사용자 결정 — b)
- 결정: `*_after` 가 delta 가 아니라 **절대 권위값**이므로 `new`(201)·`duplicate`(200) **둘 다 write-back**(덮어써도 멱등). 409·`422` 는 `*_after` 미정의라 **skip**. 시점은 LedgerWriter 가 ledger write 성공 응답을 받은 직후(별도 read round-trip 불요). write-back 부수효과는 best-effort — 예외/누락은 무시하고 ledger 멱등(REQ-019)·청산을 막지 않는다.
- 반영 위치: FR-2, FR-4, NFR-1/3
- OQ-3: 거버넌스 경로 — change-req 로 처리할지, 신규 REQ 로 추적할지?
- 상태: Answered (2026-06-08, 사용자 결정 — a: 전부 change-req)
- 결정: 기존 Implemented/Approved REQ 본문 수정(change-req)으로 처리한다. 대상 — REQ-017(어댑터 `*_after` 응답 파싱 + 결과 타입 확장), REQ-019(`LedgerWriter` write-back 배선 + SnapshotStore 주입), REQ-029(수량 source 정합 — 슬롯 오버레이 유지 명시), **REQ-030(option D 활성화 — Approved→Implemented; REQ-030 Change Log Follow-up 의 "portfolio-hub#8 배포 후 minor-change 로 option D 구현" 예고대로)**. 신규 REQ 는 생성하지 않는다. (임시 현금 추정은 REQ-029/030 범위 내 관측 갱신으로 흡수.)
- 반영 위치: 1. 거버넌스 해석, 범위 경계
- OQ-4: 체결~write-back 공백 동안 임시 현금 처리 — 추정값을 계산해 보여줄지(라벨), 아니면 마지막 권위값 + pending 플래그만 노출할지?
- 상태: Answered (2026-06-08, 사용자 결정 — (i) 추정 오버레이)
- 결정: 공백 동안 **추정 현금을 계산해 노출**한다 — `base 권위 현금 ± 미정산 체결 cash_delta`(체결가×수량 ∓ 수수료/세금), **"추정/미정산" 라벨 필수**. write-back 권위값(`cash_balance_after`) 도착 시 교체하고 해당 미정산 delta 를 제거한다. 코어 수수료(`calculate_fee`)는 근사라 추정 오차가 있으나 라벨로 커버하고, 추정 노출 구간은 outbox drain(~1s)으로 짧다. (수량은 슬롯 오버레이가 정확값이라 추정 불요 — OQ-1 B 와 짝.)
- 반영 위치: FR-3a, 4. 변경 항목, R-3
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 매수/매도 후 universe 스냅샷의 `cash_balance` 와 `asset_quantity` 가 모두 portfolio-hub 응답 권위값으로 갱신된다(현재: 수량만 live, 현금 stale).
- S-2: portfolio-hub 응답에 cash/quantity 가 없거나 불가하면 write-back 을 스킵하고 ledger 기록·청산이 정상 동작한다(best-effort).
- S-3: DUPLICATE write 응답이 cash/quantity 를 중복 적용하지 않는다(멱등).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: ledger write 응답 fixture(cash/quantity 포함) 주입 후 SnapshotStore 갱신·universe 출력 확인 / 데이터 출처: `record_trade`/`LedgerWriter` 테스트, universe 응답
- S-2 측정 방식: 응답 누락/portfolio-hub 불가 주입 시 write-back 스킵·ledger 정상 확인 / 데이터 출처: 장애 주입 테스트, outbox 상태
- S-3 측정 방식: 동일 execution_id 중복 write 시 cash/quantity 1회만 적용 확인 / 데이터 출처: 멱등 write-back 테스트
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 수량 source(OQ-1 B)·write-back 규칙(OQ-2 b)·임시 현금(OQ-4 i)·거버넌스(OQ-3 a) 확정으로 In/Out of Scope 채워짐
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3 의 테스트/검증 출처가 실제 실행 가능한 형태로 지정됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(write-back best-effort 격리)·R-2(응답 스키마 = `*_after` 확정, portfolio-hub#8 동작 중)의 해소 방식과 change-req 추적(OQ-3 a)이 확정됨. 해소 실무 책임자 배정은 REQ/batch planning 단계로 넘김(비차단).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~OQ-4 모두 `Answered`, `Open` 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Confirmed By = Justin (confirm-discovery 단계 지정)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-018_20260608_ledger-response-cash-position-writeback.md
- 참조 문서:
- 운영 환류(매수 후 cash_balance 미반영 관측) + portfolio-hub#8 요청
- docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md — REQ-029(슬롯 오버레이)/REQ-030(option D) 도입 근거
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md — ledger 기록 경로
- trex_core/adapters/ledger/portfolio_hub.py:78-94 — `record_trade` 응답 폐기 지점
- trex_core/application/ledger_outbox.py:132 — `LedgerWriter`(SnapshotStore 부재)
- trex_core/application/portfolio.py:148, 426-427 — `update_cash_position`(호출자 없음)·슬롯 오버레이
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
> 결정 완료(2026-06-08): OQ-1 수량 source = B(슬롯 오버레이 유지 + write-back 수렴) / OQ-2 write-back = b(`new`+`duplicate` 둘 다, 409·422 skip, best-effort) / OQ-3 거버넌스 = a(전부 change-req: REQ-017·019·029·030) / OQ-4 임시 현금 = (i) 추정 오버레이(라벨)+권위 수렴. DATA(portfolio-hub#8 응답 스키마)는 해소.
## DECIDE
- (없음 — OQ-1/OQ-4 확정으로 수량/현금 source 해소)
## CONFIRM
- (없음 — 거버넌스 경로 OQ-3 a 로 확정: REQ-017/019/029/030 change-req, 신규 REQ 없음)
## DATA
- (해소) portfolio-hub#8 ledger write 응답 = root flat `cash_balance_after`/`asset_quantity_after`(절대 권위값), status별 `new`(201)/`duplicate`(200)=값 있음·409/422=미정의. dcy-005 A-7 에서 확정됐고 portfolio-hub#8 **배포·동작 중**(사용자 확인 2026-06-08) — "배포 대기" 조건부 해제됨.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: confirm-discovery 완료. 저장소 사실관계 일치 확인(record_trade 는 `duplicate`만 파싱·`*_after` 미파싱 / update_cash_position 정의만 있고 호출자 없음 / LedgerWriter 에 SnapshotStore 없음 / REQ-030 Approved). 오픈 질문 OQ-1~OQ-4 모두 Answered (수량 source=B 슬롯 오버레이 유지+write-back 수렴 / write-back=b `new`+`duplicate` 둘 다·409/422 skip / 거버넌스=a 전부 change-req REQ-017·019·029·030 / 임시 현금=(i) 추정 오버레이 라벨). DATA(portfolio-hub#8 응답 스키마)는 dcy-005 A-7 `cash_balance_after`/`asset_quantity_after` 확정 + portfolio-hub#8 배포·동작 중(사용자 확인)으로 해소. C 유형 플레이스홀더 0개. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-08 21:45 KST
@@ -1,213 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-019_20260609_slot-entry-count-and-closed-reallocation
- 이슈명: slot entry_count 추가 및 closed 슬롯 재할당 정책 (issue #18)
- 작성 시각(KST): 2026-06-09 13:13 KST
- 마지막 갱신 시각(KST): 2026-06-09 13:34 KST
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 신규 REQ 없음(거버넌스 전부 change-req). 변경 REQ: REQ-002/003/006/007/009 (각 CHG-20260609-01). Delivery: BAT-025 (release-candidate).
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #18(slot entry_count 추가 + closed 슬롯 재할당 정책). 기존 dcy-001/002(슬롯 도메인·preallocation), dcy-014(일자 경계 preallocation) 참조.
- 해석:
- buy admission 은 현재 `find_idle_slot`(idle 1개 임의 선택)만 사용해 closed 슬롯을 재할당 후보로 보지 않으며, 슬롯별 진입 횟수를 기록하는 필드도 없다. issue #18 은 (1) slot-scoped `entry_count` 필드, (2) idle+closed 후보 중 `entry_count` 최소 우선 선택(+tie-break), (3) closed 직접 재할당, (4) 할당 전이 시 `entry_count` 증가를 요구한다.
- 핵심 OQ(1~3) 전부 결정됨 — 데이터 모델(마이그레이션 008)·admission 흐름·슬롯 계약(+`closed→pending_buy` 신규 전이) 변경을 동반하는 신규 주기다. **OQ-1**: 신규 `entry_count` 는 daily_slot row 단독(기존 symbol-scoped `daily_symbol_entries` 와 무관·분리). **OQ-2**: 선택은 `entry_count` 오름차순 → `updated_at` 오름차순 → `slot_id`. **OQ-3**: closed 를 admission 후보에 직접 포함(`closed→pending_buy` 직접 전이), `entry_count` 는 →pending_buy 시 +1, 동시성은 `SKIP LOCKED` 보존.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 슬롯 선택 정책(현재 idle-only `find_idle_slot`)과 슬롯 데이터 계약(`EXPECTED_SLOT_FIELDS`)에 `entry_count` 가 없어, 신규 영속 필드 + 마이그레이션 + admission 흐름 변경을 요구한다. 기존 REQ 문구 미세 조정으로 흡수되지 않는다.
- 기존 `daily_symbol_entries`(symbol-scoped 재진입 카운터, REQ-002 AC-3)와 목적·키가 다른 slot-scoped 카운터를 도입하고, closed 슬롯 재할당이라는 새 도메인 정책을 정의하므로 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **slot 진입 횟수 필드 없음**: `Slot` 모델(`trex_core/domain/slot/models.py`)에 진입 횟수 필드가 없고, `EXPECTED_SLOT_FIELDS`(test_contract.py 로 강제되는 엄격 컬럼 계약)에도 없다. daily_slot 스키마(`sql/001_create_slot_core_tables.sql`)도 slot row 에 진입 카운터를 두지 않는다.
- **admission 은 idle-only 임의 선택**: buy admission(`trex_core/application/intents.py:_admit_buy`)은 `repo.find_idle_slot`(`postgresql.py:108`, `WHERE status='idle' LIMIT 1`)으로 idle 슬롯 1개를 순서 규칙 없이 잡는다. closed 슬롯은 admission 후보가 아니며, 우선순위(진입 횟수) 개념이 없다.
- **closed 재사용은 별도 복구 전이**: 상태 머신(`domain/slot/state.py`)에 `closed→idle` 복구 전이(#7)가 있고(`service.py:130`), 이때만 슬롯이 다시 idle 후보가 된다. admission 이 closed 를 직접 후보로 보지는 않는다.
- **별개의 symbol-scoped 카운터(본 주기 범위 밖)**: `daily_symbol_entries` 테이블(`sql/001`, `trading_date+symbol → entry_count, last_entry_at`)이 `closed→idle` 복구 시 +1 증가한다(`postgresql.py:160`, REQ-002 AC-3). 이름은 같은 "entry_count" 이나 **키(symbol)·목적(심볼 재진입 결정 입력)이 slot-scoped 할당 정렬과 전혀 다르다**. 게다가 현재 **production 소비자가 없다**(`get_today_entry_count` 읽기는 테스트뿐 — 재진입 제한 게이트 미배선, trex-orbit 계보 보존 불변식 REQ-002 NFR-2). 따라서 신규 slot `entry_count` 와 **개념적으로 무관**하며, 본 Discovery 는 이 테이블을 **변경하지 않는다**(분리). 제거는 별도 트랙(아래 Out of Scope 참고).
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`)는 존재하며 최신이다. 슬롯 할당·상태 전이는 기존 runtime flow 에 속한다.
- 기대 상태
- daily_slot(슬롯 row)에 slot-scoped 진입 횟수가 영속되고, buy admission 이 idle+closed 후보 중 진입 횟수가 가장 적은 슬롯을 결정적 규칙(tie-break 포함)으로 선택한다.
- closed 슬롯이 재할당 후보로 명시적으로 취급되어 슬롯 자원이 효율적으로 재사용된다.
- 진입 횟수 증가 시점(할당 전이)이 명확히 정의되고, 신규 slot `entry_count` 는 daily_slot row 단독 source of truth 로 `daily_symbol_entries` 와 분리되어 이중 소스 모호성이 없다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물 작성
- 리뷰어/승인자 — 데이터 모델·admission 정책·카운터 정합 결정 승인
- 운영 담당자 — 슬롯 재사용·진입 이력 관측
- 전략 모듈(간접) — admission 결과(어느 슬롯이 잡히는지)에 의존하나 계약상 슬롯 선택은 core 책임
- 제약사항
- daily_slot 스키마 변경은 멱등 마이그레이션(008)으로 추가하고, `EXPECTED_SLOT_FIELDS` 계약·persistence flat slot 매핑·`data-model.md` 를 같은 변경 묶음으로 정합해야 한다(`change-req` Common Pitfall 1).
- admission 의 동시성(마지막 슬롯 경합) 보존 — 현재 `FOR UPDATE SKIP LOCKED`/재시도 규약(`intents.py`)을 깨지 않아야 한다.
- **상태 머신 불변식 변경**: 현재 "6 상태 / 8 허용 전이"(`state.py` `ALLOWED_TRANSITIONS`, REQ-001/002 NFR-2, dcy-001 FR-2)에 `closed→pending_buy` 가 없다. 본 주기는 이 전이를 추가(8→9 전이)하므로 REQ-001/002 의 전이 집합 계약·전이 카운트 서술을 change-req 로 함께 정합해야 한다.
- 신규 slot-scoped `entry_count` 와 기존 `daily_symbol_entries`(symbol-scoped, REQ-002 AC-3)는 본 주기에서 분리 유지(후자 미변경) — 회귀를 깨지 않는다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 하루 중 일부 슬롯이 진입·청산(closed)을 반복한 뒤, 새 buy 가 들어오면 admission 이 idle 뿐 아니라 closed 슬롯도 후보로 보고 **진입 횟수가 가장 적은** 슬롯을 골라 슬롯 사용을 고르게 분산한다.
- idle 슬롯이 모두 소진됐지만 closed 슬롯이 남아 있을 때, 현재는 `no_idle_slot` 으로 거부되던 buy 가 closed 재할당으로 admit 된다.
- 운영자가 슬롯별 누적 진입 횟수를 조회해 그날 슬롯 회전·집중도를 파악한다(분석 기반).
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- daily_slot/`Slot` 모델에 slot-scoped `entry_count`(기본 0) 필드 추가 + 마이그레이션 008 + `EXPECTED_SLOT_FIELDS` 계약·persistence 매핑·`data-model.md` 정합.
- buy admission 슬롯 선택 정책 변경: 후보 = idle ∪ closed, 정렬 = **`entry_count` 오름차순 → `updated_at` 오름차순(가장 오래 안 쓴 슬롯) → `slot_id`**(OQ-2), 동시성(SKIP LOCKED/재시도) 보존. `find_idle_slot` → 후보 확장 selector 로 일반화.
- closed 슬롯 재할당 경로 정의(OQ-3): admission 이 closed 를 **직접 후보**로 잡아 **`closed→pending_buy` 신규 전이**(state.py 전이 추가)로 점유 필드 초기화·설정. `entry_count` 는 할당 전이(→pending_buy) 시 +1.
- baseline 문서(`docs/runtime-flows.md` admission/슬롯 선택 흐름, `docs/data-model.md` daily_slot 필드) 갱신. (신규 `entry_count` 가 daily_slot 단독임을 명시하고, 기존 `daily_symbol_entries`(심볼 재진입 카운터)와 역할이 다름을 data-model 에 구분 기록.)
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/domain/slot/models.py` — `Slot.entry_count` + `EXPECTED_SLOT_FIELDS`
- `trex_core/adapters/persistence/sql/008_*.sql` — daily_slot `entry_count` 컬럼 추가(멱등)
- `trex_core/adapters/persistence/postgresql.py` — flat slot 매핑, 후보 selector(idle∪closed, ORDER BY entry_count, SKIP LOCKED), 증가 로직
- `trex_core/domain/slot/repository.py` / `in_memory_repository.py` — selector 포트 + 인메모리 동등 구현
- `trex_core/application/intents.py` — `_admit_buy` 후보 selector 사용 + closed 재할당 처리
- `trex_core/domain/slot/service.py` / `state.py` — **`closed→pending_buy` 신규 전이 추가**(허용 전이 집합), 할당 전이(→pending_buy) 시 `entry_count` +1, closed 직접 할당 시 점유 초기화·설정
- 문서 영향:
- `docs/data-model.md` — daily_slot `entry_count` 필드
- `docs/runtime-flows.md` — buy admission 슬롯 선택(idle∪closed, entry_count 정렬) 흐름
- 운영 영향:
- 마이그레이션 008 적용(additive 컬럼, 기본 0) — 배포 시 1회. 슬롯 선택 동작이 바뀌므로 회귀 관측 필요.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: daily_slot/`Slot` 에 slot-scoped `entry_count` 정수 필드(기본 0)를 추가하고 멱등 마이그레이션 008 + `EXPECTED_SLOT_FIELDS` 계약 + persistence flat 매핑 + `data-model.md` 를 정합한다.
- FR-2: buy admission 은 후보를 idle ∪ closed 로 확장하고, **`entry_count` 오름차순 → `updated_at` 오름차순(가장 오래 안 쓴 슬롯) → `slot_id`**(OQ-2) 순으로 결정적 선택한다. 마지막 슬롯 경합 동시성(`FOR UPDATE SKIP LOCKED` + 재시도)을 보존한다.
- FR-3: closed 슬롯은 admission 후보에 **직접 포함**되며, 선택 시 별도 `closed→idle` 왕복 없이 **`closed→pending_buy` 직접 전이**한다(OQ-3 — 신규 전이). 이때 점유 필드(symbol/qty/avg_price/limit_price/...)를 새 진입 값으로 초기화·설정한다(기존 `closed→idle` 리셋 의미 재사용).
- FR-4: slot `entry_count` 는 **할당 전이(idle/closed → pending_buy) 시 +1** 증가한다(OQ-3). 단일 트랜잭션 내에서 슬롯 claim·상태 전이·`entry_count` 증가를 함께 수행해 동시 buy 경합에서도 중복 카운트가 없다. 신규 `entry_count` 는 daily_slot row 단독이며 `daily_symbol_entries`(심볼 재진입 카운터, REQ-002 AC-3)와 **무관·독립**이다 — 본 주기에서 후자를 읽거나 변경하지 않는다.
- FR-5: `docs/runtime-flows.md`(admission 슬롯 선택)·`docs/data-model.md`(daily_slot) baseline 문서를 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 동시성 안전 — 슬롯 선택·`entry_count` 증가는 advisory lock/`SKIP LOCKED`/단일 트랜잭션으로 경합·중복 카운트 없이 처리한다.
- NFR-2: 결정성 — 동일 후보 집합·동일 `entry_count` 분포에서 tie-break 규칙으로 선택 결과가 결정적이고 재현 가능하다.
- NFR-3: 하위호환 — 마이그레이션은 additive(기본 0), 기존 idle-only 동작은 closed 후보가 없을 때 결과적으로 보존(회귀 안전).
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- slot-scoped `entry_count` 필드 + 마이그레이션 008 + 계약/매핑/data-model 정합
- admission 후보 확장(idle∪closed) + `entry_count` 정렬 + tie-break + closed 재할당 + 증가 시점
- 기존 `daily_symbol_entries` 와의 관계 확정 + runtime-flows/data-model 갱신
- Out of Scope
- 슬롯별 수익률/PnL 집계(이슈 고려사항대로 ledger/log 기반이 적절 — 본 주기 제외)
- 전략 모듈에 슬롯 선택 정책을 노출하는 계약 변경(슬롯 선택은 core 책임 유지)
- preallocation 총량/스케줄 정책 변경(dcy-014 범위 — 불변)
- **`daily_symbol_entries`(심볼 재진입 카운터) 제거/변경 — 본 주기 제외**. production 소비자 없음이 확인됐고 심볼 재진입 제한 미사용으로 결정됐으나, 보존 불변식(REQ-002 NFR-2)이라 캐주얼 삭제 불가 → **별도 REQ-002 change-req 트랙**으로 처리(후속).
- 운영자 CLI/스냅샷에 slot `entry_count` 노출 — **본 주기 제외**(이슈가 분석/노출을 후순위로 명시). 필요 시 후속 change-req 후보.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Low): 신규 slot `entry_count` 와 기존 `daily_symbol_entries.entry_count` 가 같은 이름이라 코드/문서에서 혼동될 수 있다. (OQ-1 결정으로 이중 소스 자체는 해소 — 둘은 분리·무관, 후자는 본 주기 미변경.)
- 완화 방안: data-model 에 두 카운터의 키·목적·증가 시점을 명확히 구분 기록. 신규 필드는 daily_slot 컬럼으로 테이블이 달라 물리적 충돌 없음. REQ-002 AC-3 회귀 보존(후자 미변경).
- R-2 (Medium): admission 후보에 closed 를 포함하면서 동시성(마지막 슬롯 경합)·점유 리셋이 어긋나면 잘못된 슬롯 재사용/상태 오염이 발생할 수 있다.
- 완화 방안: `FOR UPDATE SKIP LOCKED` + 단일 트랜잭션 내 상태/점유/카운트 갱신, closed 직접 할당 시 기존 `closed→idle` 리셋 로직 재사용, 경합 회귀 테스트.
- R-3 (Medium): daily_slot 컬럼 추가가 `EXPECTED_SLOT_FIELDS` 계약·flat 슬롯 read/write 순서를 깨면 광범위 회귀가 난다.
- 완화 방안: 모델·계약·persistence 매핑·data-model 을 같은 변경 묶음으로 갱신하고 `test_contract.py` green 확인.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: `entry_count` 는 trading_date 범위 내 slot row 기준(일 단위 preallocation 과 정합). 일자 경계에서 새 슬롯은 0 으로 시작한다.
- A-2: closed 슬롯은 같은 trading_date 안에서 재할당 후보다(전일 슬롯을 끌어오지 않는다 — dcy-014 trading_date 경계 유지).
- A-3: 슬롯 선택은 core admission 단독 책임이며 전략 모듈 계약에 영향을 주지 않는다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: slot-scoped `entry_count` 의 source of truth 를 어떻게 둘 것인가 — (a) daily_slot row 단독, (b) 기존 `daily_symbol_entries` 병행 유지, (c) `daily_symbol_entries` 를 slot-scoped 로 대체/재정의.
- 상태: Answered (2026-06-09, 사용자 결정)
- 결정: **(a) daily_slot row 단독.** 신규 `entry_count` 는 `daily_symbol_entries`(심볼 재진입 카운터)와 개념·키·목적이 무관 → 본 주기에서 완전 분리(후자 미변경). 심볼 재진입 제한은 사용하지 않기로 했고 `daily_symbol_entries` 는 현재 production 소비자가 없어, 그 제거는 별도 REQ-002 change-req 트랙으로 후속 처리(본 주기 Out of Scope). 반영: §3 현재 상태/기대 상태, §4, FR-4, R-1.
- OQ-2: 동률(`entry_count` 동일) 시 tie-break 규칙은 무엇인가.
- 상태: Answered (2026-06-09, 사용자 결정)
- 결정: **`entry_count` 오름차순 → `updated_at` 오름차순(가장 오래 안 쓴 슬롯) → `slot_id`**. 결정적(NFR-2) + 직관적. 반영: FR-2, §4.
- OQ-3: closed 재할당 경로와 `entry_count` 증가 시점.
- 상태: Answered (2026-06-09, 사용자 결정)
- 결정: closed 를 admission 후보에 **직접 포함**하고 별도 `closed→idle` 왕복 없이 **`closed→pending_buy` 직접 전이**(state.py 신규 전이). `entry_count` 는 **할당 전이(→pending_buy) 시 +1**. 동시성은 기존 `FOR UPDATE SKIP LOCKED` 로 보존. 반영: FR-3/FR-4, §4.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: idle 이 없고 closed 가 있는 상태에서 buy 가 closed 슬롯으로 admit 된다(이전엔 `no_idle_slot` 거부).
- S-2: 여러 후보가 있을 때 `entry_count` 최소 슬롯이 선택되고, 동률 시 tie-break 규칙대로 결정적으로 선택된다.
- S-3: 할당 전이 시 해당 slot 의 `entry_count` 가 정확히 1 증가하며, 동시 buy 경합에서도 중복 카운트가 없다. 기존 `daily_symbol_entries`(REQ-002 AC-3) 회귀가 green.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: idle=0, closed≥1 픽스처에서 admission 단위/통합 테스트 / 데이터 출처: daily_slot 상태 + admission 결과
- S-2 측정 방식: 다중 후보(상이/동일 entry_count) 픽스처에서 선택 결과 단언 / 데이터 출처: daily_slot `entry_count` + 선택된 slot_id
- S-3 측정 방식: 단일/동시 admission 후 `entry_count` 증분 단언 + `daily_symbol_entries` 회귀 테스트 / 데이터 출처: daily_slot.entry_count, daily_symbol_entries.entry_count
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 운영자 노출은 본 주기 제외로 확정. In/Out 경계 명확.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~3 측정 픽스처/데이터 출처(daily_slot.entry_count/상태)가 테스트 가능한 형태로 정의됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(이중 소스)은 OQ-1 결정(daily_slot 단독·분리)으로 Low 로 해소됨. 잔여 High 리스크 없음.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1/2/3 전부 Answered (2026-06-09 사용자 결정)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자 Justin confirm-discovery 수행
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-019_20260609_slot-entry-count-and-closed-reallocation.md
- 참조 문서:
- docs/discovery/dcy-001_20260603_slot-pure-domain-migration.md (슬롯 도메인·daily_symbol_entries 기원, REQ-002)
- docs/discovery/dcy-014_20260608_trading-date-configurable-timezone-kst.md (일자 경계 preallocation)
- docs/data-model.md, docs/runtime-flows.md (baseline — 갱신 대상)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- 없음 (OQ-1/2/3 사용자 결정 완료 — §6 참조. 운영자 노출은 본 주기 제외로 확정 — §5)
## CONFIRM
- 없음 (이슈 #18 재구성 ①entry_count 필드 ②idle/closed 중 entry_count 최소 우선 ③closed 재할당 후보 ④할당 전이 시 증가 — 사용자가 OQ-1/2/3 결정으로 확인함)
## DATA
- 없음 (외부 데이터 의존 없음 — 전부 내부 도메인/스키마 결정)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1/2/3 전부 Answered(사용자 결정 반영), C형 플레이스홀더 0, 저장소 사실 재확인 일치(slot entry_count 부재·idle-only admission·closed→pending_buy 미존재 검증). REQ Drafting 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-09 13:34 KST
@@ -1,289 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-020_20260610_asset-centric-slot-model
- 이슈명: 자산 중심 슬롯 모델 재설계 — portfolio-hub universe 와 1:1 슬롯 + 활성 슬롯 개수 제어 (일반 슬롯 풀 모델 대체)
- 작성 시각(KST): 2026-06-10 01:04 KST
- 마지막 갱신 시각(KST): 2026-06-10 01:04 KST (OQ-1 Answered: FCFS+무치환; FR-3/FR-4 holding/수량 정본 분리 결정; OQ-5 추가: 불일치 해소 정책; OQ-3 Answered: 영구 슬롯 + trading_date 강등 + dcy-014 폐기; OQ-2 Answered: 생성 즉시·제거 soft-retire/tombstone·degrade 보류; OQ-4 Answered: 포지션 사이징 분리·토대만; OQ-5 Answered: 불일치 해소 ground truth=브로커·표면화·운영자 게이트; A-1 CONFIRMED: 자산 중심 대전제; OQ-6 추가·Answered: M=per-instance TOML; review 정리 반영(§3 시나리오·§4·§11 현행화); OQ-7 추가·Answered: 공유 DB + instance_key 스코프(UNIQUE(instance_key,portfolio_id)) + advisory lock 인스턴스 스코프화; FR-6/7/8 추가(상태머신·intent 자산키·운영자 단일뷰), NFR-4/5 추가(전략 계약 호환·불일치 감지 주기), dcy-019 산출물 제거 명시, OQ-8 추가·Answered(over-cap=신규 차단·기존 유지); confirm-discovery 승인(2026-06-10 10:00 KST, Justin Ko) → 상태 confirmed; draft-req: REQ-056~065 생성)
- 대체됨: 없음
- 후속 Discovery 참조: docs/discovery/dcy-021_20260610_position-sizing-target-intent.md (OQ-4 포지션 사이징 분리 주기), docs/discovery/dcy-027_20260612_long-term-strategy-slot-operation.md (Out-of-Scope "중장기 전용 운영(issue #20)" 이월 주기)
- 생성된 REQ 참조: REQ-056(영구 슬롯 스키마), REQ-057(상태머신/lock), REQ-058(universe reconcile), REQ-059(capacity/admission), REQ-060(max_active_slots TOML), REQ-061(intent 자산 키), REQ-062(수량/불일치), REQ-063(slots 단일 뷰), REQ-064(cutover), REQ-065(baseline). 기존 REQ-002/003/005/006/007/048 등은 폐기/재정의 change-req 동반(예정).
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #1(포지션 사이징/선언형 target intent), issue #20(중장기 자산 중심 운영). 기존 dcy-001/002(슬롯 도메인·intent), dcy-005(스냅샷 universe read), dcy-014(일일 preallocation), dcy-019(slot entry_count + closed 재할당)와 직접 교차.
- 해석:
- 현재 모델은 **익명 슬롯 풀**(일일 N개 preallocation, 매수 신호 시 idle 슬롯을 자산에 동적 배정)이다. 운영 마찰(universe↔slots 표면 혼동 BAT-022, `no_idle_slot`/할당 경합, entry_count 재사용 정책 dcy-019)과 issue #1(포지션 사이징 자리 없음)·#20(자산 중심 다일 보유와 충돌)이 모두 이 풀 모델 전제에서 비롯된다.
- 본 Discovery 는 슬롯을 **portfolio-hub universe 자산과 1:1**로 두고("슬롯=자산 포지션"), capacity gate 를 "유휴 슬롯 수"에서 "**동시 활성(holding) 최대 M개**" 정책으로 바꾸는 모델 전환을 판정한다. issue #1·#20 을 흡수하는 상위 substrate 재설계이며, 기존 dcy-001/002/014/019 의 풀 모델 전제를 재정의한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 dcy 들은 모두 **익명 슬롯 풀** 전제(일일 count preallocation·idle 배정·closed 재할당) 위에 쌓였다. 자산 1:1 슬롯 + 활성 개수 제어는 슬롯의 정체성·할당·capacity 개념을 **재정의**하므로 기존 문서의 미세 조정으로 흡수되지 않는다.
- issue #1(포지션 사이징)·#20(중장기) 과 교차하나, 그 둘을 따로 풀지 않고 substrate(슬롯 모델)를 바꿔 동시에 단순화하는 독립 주기다. 기존 dcy-001/002/014/019 는 입력·영향(폐기/재해석 범위)으로 참조한다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **익명 슬롯 풀 + 일일 count preallocation**: `preallocate_daily_slots(trading_date, count)`(`preallocation_scheduler.py`, dcy-014)가 매일 `TREX_TOTAL_SLOTS`(기본 10)개의 idle 슬롯을 생성한다. 슬롯은 특정 자산에 묶이지 않은 익명 capacity 다.
- **매수 신호 시 동적 배정**: buy admission(`intents.py:_admit_buy`)이 `find_allocatable_slot`(idle∪closed, dcy-019/BAT-025)로 슬롯 1개를 잡아 portfolio_id 를 채운다. capacity gate 는 `available_slots = idle_count`(`portfolio.py`/`messaging_handlers.py`).
- **universe 와 slots 가 별개 표면**: universe(자산 리스트)는 portfolio-hub `get_portfolios(daemon_id)`(`hub_read.py`, `managed_by_daemon_id` 필터)로 읽고, slots(슬롯 상태)는 별도다. 둘의 임피던스가 운영 혼동(BAT-022: `slots` 가 universe 를 슬롯으로 오인 표시)과 정합 마스킹(REQ-031: hub 수량 vs 브로커 수량 불일치 시 universe 마스킹)을 낳았다.
- **포지션 사이징 자리 없음**: 상태머신(`state.py`)이 이산적(IDLE→PENDING_BUY→HOLDING→PENDING_SELL→CLOSED)이라 `HOLDING→HOLDING`(수량 변경)·부분 매도가 없다(issue #1).
- **일일 리셋 vs 영구 자산**: 슬롯이 trading_date 로 일일 preallocation 되어 다일 보유 자산 모델(issue #20)과 충돌한다.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`)는 존재·최신이며 슬롯 풀·preallocation·admission 흐름을 기술한다 — 본 전환 시 대폭 갱신 대상.
- 기대 상태
- 슬롯이 portfolio-hub universe 자산과 **1:1**로 대응되어 "슬롯=자산 포지션"이 된다. 익명 배정·할당 경합·closed 재할당 개념이 사라진다.
- capacity 제어가 "**동시 활성(holding) 슬롯 최대 M개**" 정책으로 표현되어, 활성화는 FCFS(신호 도착 순)이고 M 이 차면 새 신호를 거부한다(코어 자율 치환 없음, OQ-1). 능동적 capacity 배분은 전략 선언형 target(OQ-4)으로 분리한다.
- universe↔slots 표면이 합쳐져 운영 관측이 단순해진다. 다일 보유(#20)는 영구 슬롯으로 이번 주기에 실현되고, 포지션 사이징(#1)은 본 토대 위 **후속 주기**로 얹힌다(OQ-4 분리).
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물
- 리뷰어/승인자 — 슬롯 모델·capacity 정책·기존 작업 폐기 범위 승인
- 운영 담당자 — 자산별 포지션·활성 수 관측
- 전략 모듈 — 슬롯=자산이면 신호·소유 시맨틱(dcy-005 OQ-9)이 바뀜
- portfolio-hub 팀 — universe 가 슬롯 집합의 source of truth 가 됨(자산 추가/제거 동기화)
- 제약사항
- 검증·출시된 풀 모델 경로(REL-001~004, BAT-001~023)와 방금 출시한 dcy-019/BAT-025(entry_count·closed 재할당)를 **재정의/폐기**해야 한다 — 회귀·이행 비용이 크다.
- portfolio-hub universe 가 변동(자산 추가/제거)하므로 슬롯 생성/소멸 lifecycle 을 스냅샷 refresh 와 결합해야 한다.
- 단일 writer·동시성·불변식(advisory lock, REQ-002 NFR-1)을 새 모델에서도 보존해야 한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- portfolio-hub 가 45개 자산을 제공하면 trex-core 에 45개 자산 슬롯이 생기고, 운영자는 "동시 활성 최대 M개" 만 제어한다 — 어떤 자산을 살지는 슬롯 배정이 아니라 활성 선택으로 결정된다.
- 매수 전략이 자산 A 신호를 주면 A 의 슬롯이 활성화(holding)되고, 활성 수가 M 미만이면 즉시 활성화, M 이 차 있으면 거부된다(FCFS·코어 자율 치환 없음, OQ-1). 회전은 전략의 명시적 sell→buy 로 한다.
- (후속 주기) 보유 중 전략이 목표 수량을 바꾸면(issue #1) 같은 자산 슬롯의 수량이 조정된다(별도 슬롯 불필요) — 본 주기는 토대만, 포지션 사이징은 분리(OQ-4).
- 운영자가 `trex-core slots` 로 자산별 포지션·활성 여부를 한 표면에서 본다(universe↔slots 분리 해소).
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 슬롯 정체성 재정의: 익명 풀 → **portfolio-hub universe 자산과 1:1 바인딩**(슬롯 = 자산 포지션, portfolio_id 영구 키).
- 슬롯 lifecycle 재설계: 일일 count preallocation(dcy-014) → **universe 기반 슬롯 생성/소멸**(스냅샷 refresh 와 결합). idle 풀·`find_allocatable_slot`·closed 재할당(dcy-019) 폐기/대체.
- 슬롯 영속성 전환(OQ-3): 일일 리셋 → **영구 슬롯**(키=`portfolio_id`, `UNIQUE`, 날짜 없음). `daily_slot.trading_date` 컬럼 드롭/재의미, trading_date 는 이벤트/원장/정책의 시간 속성으로 강등. 일일 daily_slot 행 → 자산당 영구 1행 cutover + 보유 carry-over.
- capacity 제어 전환: `available_slots`(유휴 수) → "**동시 활성 holding 최대 M개**" 정책. 활성 수 == M 이면 신규 신호 거부(FCFS·무치환, OQ-1). M 값은 per-instance TOML 설정에서 읽는다(OQ-6).
- 상태머신 재해석(FR-6): 익명 슬롯의 IDLE/CLOSED → 영구 슬롯의 **단일 `inactive`** 로 합침(`closed→idle` 복구 전이 제거), 집합 = `inactive→pending_buy→holding→pending_sell→inactive`(+`error`). 활성=진입, 종료=전량 청산. **HOLDING→HOLDING 수량 변경·부분 매도 미포함**(포지션 사이징 OQ-4 분리). 수량 가변 전제는 모델에 남김.
- intent 계약 통일(FR-7): 매수·매도 모두 portfolio_id(자산) 키, `sell-by-slot_id` 폐기, 중복/무효 신호 멱등 거부(`already_active`/`not_holding`).
- 운영자 표면 단일화(FR-8): `trex-core slots` = 자산 포지션 단일 뷰(universe↔slots 이중 표면 해소, BAT-022).
- dcy-019 산출물 제거(FR-5): `find_allocatable_slot`·`closed→pending_buy` 전이·slot `entry_count` 제거(FCFS·영구 슬롯이 대체). `daily_symbol_entries` 폐기.
- baseline 문서(`docs/project-structure.md`/`docs/runtime-flows.md`/`docs/data-model.md`) 전면 갱신 + 기존 dcy-001/002/014/019 와의 대체/재해석 관계 명시.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/domain/slot/` — 상태머신(state.py)·서비스(service.py)·모델(models.py): 슬롯 정체성·전이·capacity 개념 재정의
- `trex_core/application/preallocation_scheduler.py` — count preallocation → universe 기반 생성/소멸로 대체
- `trex_core/application/intents.py` — `find_allocatable_slot` 배정 경로 → 자산 슬롯 활성화/선택 경로
- `trex_core/application/snapshot_scheduler.py` / `portfolio.py` — universe fetch 와 슬롯 집합 동기화 결합, `available_slots`→활성 수 개념
- `trex_core/adapters/persistence/postgresql.py` + 마이그레이션 — daily_slot 스키마(자산 키·활성 플래그·`retired_at`·`instance_key`) 재설계, `UNIQUE(instance_key, portfolio_id)`; slot-scoped 테이블 전반 instance 스코프화 + 모든 쿼리에 instance 필터(OQ-7)
- `trex_core/domain/slot/state.py`·`service.py` — advisory lock 인스턴스 스코프화(`(SLOT_LOCK_ID, hash(instance_key))`, NFR-1 재정의, OQ-7)
- `trex_core/runtime/config.py`(`DaemonInstanceConfig`) + `config/*.toml` — 동시 활성 최대치 M 설정 키(예: `max_active_slots`) 추가(OQ-6, REQ-048 per-instance TOML 계열)
- 문서 영향:
- `docs/data-model.md`(daily_slot 재설계: instance_key·retired_at·entry_count 제거·trading_date 강등), `docs/runtime-flows.md`(admission→활성화 흐름·reconcile lifecycle·불일치 해소), `docs/project-structure.md`(슬롯 모델)
- `trex_core/domain/slot/state.py`·`models.py` — 상태 집합 재정의(FR-6: idle/closed→inactive), `entry_count` 제거
- `trex_core/application/intents.py`·메시지 계약 — intent 자산 키 통일·sell-by-slot_id 폐기(FR-7); `trex_core/runtime/cli.py` slots 단일 뷰(FR-8)
- (제거) `daily_symbol_entries` 테이블·`find_allocatable_slot`·`closed→pending_buy` 전이(dcy-019 산출물)
- 운영 영향:
- 슬롯 수가 universe 크기에 연동, capacity 운영이 "활성 M개" 정책으로 전환. 마이그레이션·이행 절차 필요. 기존 풀 모델 운영 가정 변경.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 슬롯은 portfolio-hub universe 의 자산과 1:1 로 대응한다(자산 portfolio_id 가 슬롯의 **영구 키**, 날짜 없음 — OQ-3). **다중 인스턴스는 DB 를 공유하고 슬롯은 `instance_key` 로 스코프**하므로 슬롯 유일성은 **`UNIQUE(instance_key, portfolio_id)`** 이다(OQ-7). `daily_slot` 에 `instance_key` 컬럼 추가, slot-scoped 테이블 전반(slot_transition_log/intent_log/ledger_outbox/snapshot_pause/killswitch_exclusion 등)도 instance 스코프화·쿼리 필터. `daemon_id` 는 universe 파티션(get_portfolios)·감사용으로 (선택) 병행 저장하되 스코프 키는 아니며, instance_key↔daemon_id 1:1 정합을 가드한다(OQ-7). 슬롯은 **영구**이며 날 경계로 리셋되지 않고, universe 동기화(reconcile, OQ-2) 시: 신규 자산 → 슬롯 즉시 생성(비활성·멱등); 제거 자산 → **비활성이면 soft-retire(행 보존, `retired_at`/`universe_detached` 마킹, hard delete 금지)·보유 중/pending 이면 tombstone(safety-sell 유지·신규 매수 제외, 닫힌 뒤 은퇴)**. reconcile 은 스냅샷 refresh 경로(부팅+매일 07:30 KST, +운영자 on-demand)에서 단일 writer(advisory lock)로 수행. `retired_at`/`universe_detached` 는 status 와 직교한 마커. 슬롯 행의 기존 `trading_date` 컬럼은 드롭/재의미하고, trading_date 는 이벤트/원장/정책의 시간 속성으로 강등(OQ-3). 일일 preallocation(dcy-014)은 폐기하고 슬롯 생성을 universe reconcile 로 대체.
- FR-2: capacity 는 "동시 활성(holding) 슬롯 최대 M개"로 제어한다. 활성화는 **FCFS(신호 도착 순)**이며 활성 수 == M 이면 새 신호를 **거부(`capacity_full`)**한다 — 코어는 자리를 만들려고 기존 holding 을 자율 청산(치환)하지 않는다(OQ-1). 도착 순서는 단일 writer 직렬화로 결정적이고, 동시 후보 tie-break 은 `updated_at` asc → `portfolio_id`(alpha 비사용). capacity 를 최고 기회에 배분하는 능동 선택이 필요하면 전략의 선언형 target(OQ-4/issue #1)으로 처리한다. **M 값은 per-instance TOML 설정(`config/*.toml`, 예: `max_active_slots`)에서 읽는다**(REQ-048 per-instance TOML 계열, OQ-6) — 인스턴스별 독립 운영.
- FR-3: 매수/매도 신호는 익명 슬롯 배정이 아니라 **해당 자산 슬롯의 활성화/비활성화**로 처리한다. 동시성·단일 writer·불변식(advisory lock)을 보존한다. **holding(포지션 열림 여부)은 코어 슬롯의 로컬 정본**으로, 체결 시 동기 set·매도 체결 시 동기 clear 한다 — portfolio-hub `asset_quantity` 로 판단하지 않는다. 근거: (1) 자율 안전 매도(forced_close/stop_loss/take_profit/market_close/watchdog/kill-switch)는 portfolio-hub 불가 시에도 독립 동작해야 하므로(REQ-024) 보유 사실을 코어 durable 상태에서 알아야 한다; (2) universe 에서 자산이 빠지면 `get_portfolios` 에서 사라져 `asset_quantity` 신호가 증발하지만 로컬 holding 은 생존(OQ-2 고아 포지션 방지); (3) hub 수량은 ledger outbox 비동기 전파(dcy-015)라 지연되므로 안전 판단의 primary 신호로 부적합; (4) capacity(M개, FR-2) 카운트의 원자성은 단일 writer 슬롯 상태라야 보장. (사용자 결정 2026-06-10)
- FR-4: 보유 **수량(asset_quantity)의 정본은 portfolio-hub**이며, 슬롯은 수량을 정본 소유하지 않고 **로컬 best-effort 미러**(option D write-back `*_after`·추정 오버레이, BAT-023)로만 둔다. 실제 청산 시 매도 수량은 **브로커 잔고(`BrokerPort.get_balance().hldg_qty`)를 정본**으로 조회한다 → 로컬 미러가 stale 해도 청산 정확. 브로커 잔고는 포지션 존재·수량 불일치의 **reconcile ground truth** 이기도 하다(OQ-5). 즉 "포지션 열림(boolean)=코어 로컬, 수량=hub 정본+로컬 미러"로 분리. (issue #1 연계) 활성 슬롯의 보유 **수량 변경(포지션 사이징)** 은 본 주기에서 **분리(OQ-4 Out of Scope)** — 단 수량 가변 전제는 모델에 남긴다(토대 가드). 슬롯이 `qty`/`avg_price` 를 정본 소유하던 풀 모델(reconcile_positions/REQ-031 warning, option D write-back 의 drift 원인)은 본 분리로 정리한다. (사용자 결정 2026-06-10)
- FR-5: `docs/data-model.md`·`docs/runtime-flows.md`·`docs/project-structure.md` 를 새 모델로 갱신하고, dcy-001/002/014/019 의 폐기/재해석 범위를 명시한다. **dcy-019 산출물 구체 처분**: `find_allocatable_slot` selector·`closed→pending_buy` 전이·slot `entry_count` 필드는 FCFS·영구 슬롯 전환으로 **제거**(슬롯 회전 분석이 필요하면 `slot_transition_log` 로 도출, 슬롯 컬럼 아님). `daily_symbol_entries`(symbol-scoped 재진입 카운터)도 자산 중심 모델에서 무의미 → 폐기(별도 트랙과 합류).
- FR-6: **슬롯 상태머신을 자산 영구 슬롯에 맞게 재정의**한다. 기존 `idle`(매수 전)·`closed`(매도 후)는 영구 슬롯에서 **단일 `inactive`(포지션 없음)** 로 합쳐지고(dcy-001 `closed→idle` 복구 전이 제거), 상태 집합 = `inactive → pending_buy → holding → pending_sell → inactive`(+ 모든 비-error → `error`, `error → {inactive, holding}` 운영자 resolve). `retired_at`/`universe_detached`(OQ-2)·`instance_key`(OQ-7)는 status 와 **직교한 속성**으로 둔다. 활성=진입, 종료=전량 청산(HOLDING→HOLDING·부분 매도 미포함, OQ-4).
- FR-7: **intent 계약을 슬롯 native 키(`slot_id`)로 통일**한다(2026-06-10 재정의 — 아래 주). 매수·매도 모두 `slot_id` 로 식별하고 스냅샷 `slots[]` 가 계약 1차 단위; portfolio_id 는 표시/자산 정체성으로 잔존(계약 키 아님). 매수 = 그 슬롯 활성화(`inactive→pending_buy`, symbol 은 슬롯 portfolio_id 로 resolve), 매도 = 비활성화(`holding→pending_sell`). **중복/무효 규칙**: 이미 활성(`pending_buy`/`holding`/`pending_sell`)인 슬롯에 매수 → `already_active`; 보유(holding) 아닌 슬롯에 매도 → `not_holding`. intent_id 멱등 보존.
- **[재정의 2026-06-10, 사용자 결정 / REQ-061 CHG-20260610-03]** 최초 FR-7 은 `portfolio_id` 키 통일(+sell-by-slot_id 폐기)였으나, trex-core 가 슬롯 기준 관리이고 스냅샷이 `slots[]` 를 단위로 주므로 **코어 native 키 `slot_id` 로 통일**하는 것이 일관적이고 portfolio-hub 식별 체계의 계약 누수를 막는다. 자산 영구 슬롯이라 매수도 slot_id 로 지정 가능(슬롯 선존재). 매도는 이미 slot_id(불변); 매수만 portfolio_id→slot_id 이동(레거시 portfolio_id dual-accept, 폐기 BAT-δ). slot_id 는 계약 키이자 내부 PK 로 잔존(제거 아님).
- FR-8: **운영자 표면 단일화** — `trex-core slots` 가 자산 포지션(슬롯=자산, status·holding·수량 미러·`universe_detached`/`retired`)을 **단일 뷰**로 보여준다. universe↔slots 이중 표면(BAT-022 혼동 원인) 해소. universe(자산 리스트)와 slots(자산 포지션)가 같은 자산 집합 위 view 가 된다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 동시성·일관성 — 슬롯 활성화/수량 변경은 advisory lock/단일 트랜잭션으로 경합 없이 처리(REQ-002 NFR-1 계승). **공유 DB 다중 인스턴스에서 단일 writer 불변식은 인스턴스 단위로 재정의**한다: advisory lock 을 인스턴스 스코프화(예: `pg_advisory_xact_lock(SLOT_LOCK_ID, hash(instance_key))`)해 인스턴스 간 교차 경합(한 인스턴스 전이가 타 인스턴스를 막음)을 제거한다(OQ-7).
- NFR-2: 이행 안전 — 기존 풀 모델 데이터/운영에서 새 모델로의 마이그레이션이 검증된 매수/매도 경로(REL-001~004)를 회귀시키지 않거나, 명확한 cutover 절차를 갖는다.
- NFR-3: universe 결합 견고성 — portfolio-hub 불가/degraded 시 슬롯 생성·은퇴를 **전부 보류**하고 마지막 집합을 유지한다(`SnapshotStore` degraded 신호 재사용). fetch 성공이라도 universe 가 N→0/급격 대량 감소면 글리치로 의심해 **mass-retire 보류 + 경고**(정상 단건 제거와 구분). 보유 중 자산은 universe 누락에도 tombstone 으로 보호되어 포지션을 잃지 않는다(OQ-2). 코어 holding↔브로커/hub 의 **비정상 불일치**는 브로커를 ground truth 로 감지(REQ-031 reconcile 확장)하되 조용한 자동 교정 없이 warning/`error`+운영자로 표면화하고, 실주문 발생은 운영자 게이트로 둔다(OQ-5).
- NFR-4: 전략 계약 호환 — intent 계약 변경(FR-7: 자산 키 통일, `sell-by-slot_id` 폐기)·snapshot envelope 의미 변경(FR-8)이 외부 전략 모듈을 깨뜨리므로, **계약 버전/cutover 절차**를 REQ/배치에서 명시한다(전략 모듈 동시 전환 또는 한시적 하위호환). 슬롯 선택은 여전히 core 책임(전략은 자산 신호만, A-3 계승).
- NFR-5: 불일치 감지 주기 — 코어 holding↔브로커 비정상 불일치(OQ-5 Case B, 미보호 고아 포지션)는 안전 민감이므로, 감지 reconcile 은 일일 universe refresh 와 **분리해 더 자주** 수행 가능해야 한다(브로커 잔고+슬롯 상태만 필요, 전체 universe fetch 불필요). 구체 주기는 설계 tuning.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 슬롯=자산 1:1 영구 모델 + 활성 개수 제어(M, FCFS+무치환) 정책 정의
- 슬롯 lifecycle 의 universe 동기화(생성/soft-retire/tombstone) 설계
- 기존 풀 모델(일일 preallocation/idle 배정/closed 재할당) 폐기·재해석 + 일일행→영구행 cutover 범위 산정
- holding=코어 로컬·수량=hub 정본+미러 분리(FR-3/FR-4)
- baseline 문서 재설계 요구
- 토대 가드: 수량 가변 전제 유지(고정 가정 금지) + "활성=진입/종료=전량 청산" 상태 경계 고정(후속 사이징 얹힘 대비)
- Out of Scope
- **포지션 사이징(활성 슬롯 보유 수량 변경)·선언형 target intent(issue #1) — 분리(OQ-4), 별도 후속 Discovery.** 본 주기는 HOLDING→HOLDING 수량 변경·부분 매도를 구현하지 않는다.
- 중장기 전용 운영(issue #20)의 전체 — 본 모델은 토대만 제공(다일 보유는 영구 슬롯으로 실현)
- 코어 holding↔broker/hub 비정상 불일치 해소 정책 — OQ-5(잔여 Open)
- 전략 모듈 내부 구현(외부 repo)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 검증·출시된 풀 모델(REL-001~004, 특히 방금 출시한 dcy-019/BAT-025)을 폐기/재정의 — 광범위 회귀·재작업·이행 위험. OQ-3(영구 슬롯) 확정으로 **일일 daily_slot 행 → 자산당 영구 1행 cutover + 현재 보유 포지션 carry-over** 가 본 주기 최대 작업으로 구체화됨. 일일 preallocation(dcy-014) 폐기, `daily_slot.trading_date` 스키마 변경 동반.
- 완화 방안: 영향 분석으로 폐기 범위를 명시하고, cutover/마이그레이션(일일행→영구행, 보유 carry-over, **기존 데이터 instance_key 태깅 + slot-scoped 테이블 instance 스코프화**, OQ-7) 절차와 회귀 테스트를 REQ/배치 단계에서 우선 설계. trading_date KST 계산(REQ-005)은 일일 회계 경계용으로 존속(부분 재해석). 단계적 전환(읽기 정합 → 모델 전환) 검토.
- R-2 (완화): universe 변동에 따른 슬롯 생성/소멸이 보유 포지션·진행 중 주문과 충돌(예: 보유 중 자산이 universe 에서 빠짐).
- 완화 방안: OQ-2 확정 — 보유 중/pending 슬롯은 **tombstone**(은퇴 금지, safety-sell 유지, 닫힌 뒤 은퇴), 비활성만 soft-retire(행 보존), degrade·대량소멸 시 보류(NFR-3). 잔여는 비정상 divergence(OQ-5)로 분리.
- R-3 (해소): "활성 M개" 정책의 선택/치환 규칙이 모호하면 비결정적·불공정 활성화.
- 완화 방안: OQ-1 을 FCFS+무치환(단일 writer 직렬화로 도착 순서 결정적, tie-break `updated_at`→`portfolio_id`)으로 확정 — 치환 분기 자체를 제거해 모호성 해소. 재현성은 테스트로 보장.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 제품 방향이 "알려진 universe 자산의 포지션 관리 + 동시 보유 수 제어"(자산 중심)이며, 익명 capacity 풀(기회주의적 자본 배분)이 아니다. — **CONFIRMED (2026-06-10, 사용자 확정)**. 모델 전환의 대전제 성립 → OQ-1~5 결정의 기반.
- A-2: portfolio-hub universe(`get_portfolios`, `managed_by_daemon_id` 필터)가 슬롯 집합의 source of truth 로 적절하다.
- A-3: 단일 writer·advisory lock 불변식은 새 모델에서도 유지 가능하다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 활성 후보가 "최대 M개"를 초과할 때 활성 자산 선택/치환 규칙은 무엇인가(우선순위·tie-break·기존 보유 우선 등).
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **FCFS(신호 도착 순) + 가득 차면 거부, 코어 자율 치환(eviction) 없음.** 활성 수 < M 이면 신호 온 자산 즉시 활성화, 활성 수 == M 이면 새 신호 거부(`capacity_full`) — 자리를 만들려고 기존 holding 을 코어가 임의 청산하지 않는다. 도착 순서는 단일 writer + advisory lock 직렬화(intent_log 시퀀스)로 이미 결정적이며, 동시 후보가 실제로 생기는 경로(재기동·정합)의 tie-break 은 alpha 가 아닌 내재적 결정 키(`updated_at` asc → `portfolio_id`)로 한다.
- 근거: (1) "코어는 집행, 전략은 alpha" 분리 보존 — 어떤 자산이 더 좋은지(살지/뺄지)는 전략 책임이며, 코어가 score 로 후보를 줄 세우면 alpha 판단을 흡수한다(dcy-019 A-3 계승). (2) 치환=자율 청산이라 사용자 채택 "코어 자율 청산 안 함" 원칙(issue #15)과 충돌 — 회전이 필요하면 전략이 명시적 sell→buy 로 수행(감사 로그에 의도 잔존). (3) 결정성(NFR-1/R-3)이 직렬화로 공짜로 따라온다.
- 트레이드오프 & 연동: FCFS 는 확신 낮은 이른 신호가 슬롯을 점유해 더 좋은 후속 신호를 막을 수 있다. "M개 capacity 를 최고 기회에 배분"이 필요하면 그것은 **전략이 'M개 목표 포트폴리오'를 선언**하고 코어가 reconcile 하는 모델(OQ-4 / issue #1)로 처리한다 — 그 경우 선택 규칙은 전략 선언으로 이동하고 OQ-1 은 "target 이 M 초과면 선언 검증 단계에서 거부"가 된다. 본 결정은 OQ-4 분리(토대만) 전제에서의 단순 규칙이며, 선언형 target 채택 시 자연 확장된다.
- 반영: FR-2, R-3, §3 기대 상태, §10 DECIDE.
- OQ-2: universe 변동 시 슬롯 lifecycle — 신규 자산 슬롯 생성 시점, 제거 자산(특히 보유 중) 슬롯의 소멸/청산/보류 규칙, 스냅샷 refresh 와의 결합 방식.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정 (영구 슬롯 OQ-3·holding 로컬 FR-3 전제):
- **생성**: universe 신규 자산 → 영구 슬롯 즉시 생성(비활성). `get_portfolios` reconcile 경로에서 수행, `UNIQUE(portfolio_id)` 멱등(있으면 no-op). fetch 성공 시에만.
- **제거 — 비활성**(holding 아님 + pending 주문 없음): **soft-retire**(행 보존, `retired_at`/`universe_detached` 마킹). **hard delete 금지** — `slot_transition_log` 이력 보존 + 같은 자산 재추가 시 동일 슬롯 재활성화(영구 portfolio_id 키).
- **제거 — 보유 중/ pending 진행 중**: **은퇴 금지, tombstone**. 슬롯 유지 + safety-sell/watchdog 계속 동작(FR-3), universe-detached 로 마킹해 **신규 매수 후보 제외**하되 기존 포지션은 보호·매도 가능. 슬롯이 닫힌(매도 체결) **후 은퇴**. 코어 자동 청산 안 함(issue #15).
- **cadence**: 기존 스냅샷 refresh(`run_once()` 의 `store.swap` 직후, 부팅 + 매일 07:30 KST)에 reconcile 을 얹는다 — 별도 빠른 루프 불필요(영구 슬롯이라 제거 지연이 tombstone 으로 안전). 즉시성 필요 시 운영자 on-demand reconcile(CLI) 허용.
- **degrade + 대량소멸 가드**(NFR-3): portfolio-hub 불가/degraded → 생성·은퇴 전부 보류, 마지막 집합 유지. fetch 성공이라도 universe 가 N→0/급격 대량 감소면 글리치 의심 → mass-retire 보류 + 경고(정상 단건 제거와 구분).
- **단일 writer**: 생성/은퇴 쓰기도 전이와 같은 advisory lock 경로(스냅샷 스레드가 repo 통해).
- **새 마커**: `retired_at`(또는 `universe_detached` boolean) — 포지션 lifecycle(status)과 **직교**(status enum 에 'retired' 넣지 않음). "보유 중 + universe-detached" 조합 표현용.
- **capacity 상호작용**: tombstone-holding 슬롯은 실포지션이므로 M 카운트 포함(닫힐 때까지), 단 재매수 불가.
- 반영: FR-1, NFR-3, §4, R-2, §10 DECIDE. OQ-5(비정상 divergence)와 경계 구분 — universe-detached holding 은 예상된 정상 케이스(tombstone), 코어-holding↔broker-0 같은 비정상은 OQ-5(error/operator).
- OQ-3: trading_date 의미 재정의 — 자산 슬롯은 영구적인데 현재는 일일 리셋. 일/다일 경계, 일일 회계와의 관계(issue #20 직결).
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **영구 슬롯.** 슬롯 키 = `portfolio_id`(날짜 없음; 다중 인스턴스 공유 DB 라 실제 유일성 제약은 OQ-7 에서 `UNIQUE(instance_key, portfolio_id)` 복합키로 정련), 슬롯 행은 날 경계로 리셋되지 않고 자산이 universe 에 있는 동안 영속(holding 이 날 경계를 그대로 넘어감 — issue #20). **trading_date 는 슬롯의 정체성에서 제거되고, 이벤트/원장/정책의 시간 속성으로 강등**된다: 일일 활동·회계는 슬롯이 아니라 `slot_transition_log`(ts)·ledger·일일 집계를 날짜로 필터해서 본다. 슬롯 행의 기존 `trading_date NOT NULL` 컬럼은 드롭하거나 의미 변경(`opened_at`/`last_activity_date` 등)한다(스키마 변경). 일일 preallocation(dcy-014 `preallocate_daily_slots`/00:00 트리거)은 **폐기**하고 슬롯 생성/소멸을 universe reconcile(OQ-2)로 대체한다 — 단 trading_date 의 KST 계산 자체(dcy-014/REQ-005)는 일일 회계 경계용으로 **일부 존속(재해석)**, 완전 폐기 아님.
- 근거: 슬롯=자산 포지션은 본질적으로 다일 보유이므로 일일 리셋과 충돌. 영구 슬롯은 풀 모델 부산물(일일 preallocation·`no_idle_slot`·closed 재할당 dcy-019·00:00 경계 미생성 issue #16)을 개념적으로 소멸시키고, lifecycle 을 날짜 축에서 떼어 universe 멤버십에만 반응하게 해 OQ-2 와 FR-3(holding 로컬 영속)에 정합한다.
- 반영: FR-1, §3 현재/기대 상태, §4, §영향 범위, R-1, §10 DECIDE. dcy-014 폐기/재해석 범위 명시(FR-5).
- OQ-4: 포지션 사이징(issue #1, 활성 슬롯 수량 변경)을 본 주기에 포함할지, 토대만 깔고 별도 주기로 분리할지.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **분리 (Out of Scope). 본 주기는 토대만.** 포지션 사이징(활성 슬롯 보유 수량 변경)과 선언형 target intent(issue #1)는 **별도 후속 Discovery** 로 둔다.
- 근거: dcy-020 은 이미 substrate 재설계 + 일일행→영구행 cutover(R-1 High) + 스키마 변경 + dcy-014/019 폐기로 거대하다. 신규 거래 능력(수량 변경)을 얹으면 모델 마이그레이션과 뒤엉켜 회귀 분리·cutover 검증이 불가능해진다. 게다가 우리가 정한 토대(영구 자산 슬롯·holding 로컬·수량 hub 정본+미러·청산 수량 브로커 조회, FR-3/FR-4)가 사이징을 깔끔히 받쳐주므로 미뤄도 손해 없이 후속이 더 쉬워진다. 선언형 target 은 intent 계약 변경(자체 OQ 묶음: 델타 계산·부분체결·오버슈트·rebalancing·M 캡 상호작용)이라 독립 주기가 맞다. issue #20(다일 보유)의 핵심 가치는 영구 슬롯(OQ-3)으로 이미 실현.
- **토대 가드(이번 주기 필수, 재작업 방지)**: (1) 수량 고정 가정을 모델/스키마/제약에 박지 않는다(수량=hub 정본+미러라 자연히 가변). (2) 상태 의미 경계 고정 — "활성"=포지션 진입(크기는 기존처럼 allocated_capital/qty 로 1회 결정)·종료 시 전량 청산. **HOLDING→HOLDING 수량 변경·부분 매도는 본 주기에서 구현하지 않는다.**
- 반영: §범위 경계 In/Out Scope, §4 상태머신 항목, FR-4, §10 DECIDE.
- OQ-5: 코어 로컬 holding(FR-3) 과 portfolio-hub `asset_quantity`/브로커 잔고가 **실제로 어긋날 때** 해소 정책 — 누가 이기고 무슨 행동을 하는가(예: 코어는 holding 인데 브로커 0, 또는 그 반대). 수량 정본 분리(FR-3/FR-4)에서 파생된 잔여 결정.
- 상태: Answered (2026-06-10, 사용자 결정)
- 권위 모델: ① 즉시 안전 행동 구동 = **코어 로컬 holding**(FR-3, offline); ② 포지션 존재·수량의 reconcile **ground truth = 브로커 잔고**(`get_balance().hldg_qty`, 실계좌); ③ hub `asset_quantity` = 회계(코어 ledger write 의 downstream, 지연 정상 → 대부분 warning, 코어 상태 오류 아님).
- 케이스별 해소:
- **A. 코어 holding / 브로커 0**(유령 포지션): 조용한 자동 flat 금지 → 슬롯 **`error`(REQ-004 reconcile_mismatch) + WARN + 운영자 resolve**. error 인 동안 safety-sell 대상 제외(유령 청산 시도 중단).
- **B. 코어 flat / 브로커 보유**(관리 안 되는 고아 포지션, **최위험**): 큰 **WARN/alert + quarantine/error 격리 + 운영자 결정**(관리 편입 vs 청산 vs 무시). **자동 청산 금지(issue #15)**, **임계값 조작 자동 편입 금지**(cost basis·stop/take 부재 — 편입해도 시간기반 forced/market-close·kill-switch 만 가능).
- **C. 둘 다 보유, 수량만 불일치**(holding boolean 일치): 수량은 코어 정본 아님(FR-4) → **브로커 기준 미러 갱신**, hub 지속 불일치는 warning(REQ-031). **상태 변경 없음.**
- **D. 코어+브로커 일치, hub 만 다름**(hub 회계 지연): 기존 ledger outbox write-back/재시도(dcy-015/018) + warning. **코어 상태 문제 아님.**
- 원칙: (1) **조용한 자동 교정 금지·표면화** — 자동은 비파괴(미러 갱신/warning/error·quarantine 격리)만, 실주문 발생·취소(청산 등)는 **운영자 게이트**(issue #15 자율청산 금지 일관). (2) **감지 = `reconcile_positions`(REQ-031) 확장** — 브로커 잔고 + hub 수량 + **코어 슬롯 holding** 3자 비교. cadence 는 Case B 안전 민감성 때문에 일일 universe refresh 보다 자주 가능(브로커 잔고+슬롯 상태만 필요, 전체 universe fetch 불필요) — 빈도는 tuning.
- 경계: **universe-detached 인데 holding = 정상(tombstone, OQ-2)** → OQ-5 아님. OQ-5 는 세 출처의 **비정상 충돌**(A/B/C/D)만.
- 기존 자산 재사용(green-field 아님): 감지=REQ-031, 격리/해소=`error`+운영자 resolve(REQ-004), 가시화=warning 오버레이 + safety-sell 카운터/구조화 로그(dcy-017).
- 반영: OQ-5 본문(상기), FR-4(브로커 reconcile ground truth), NFR-3, §10 DECIDE.
- OQ-6: 동시 활성 최대치 M 의 값·설정 출처는 무엇인가(env / per-instance TOML / 기본값). 기존 `TREX_TOTAL_SLOTS`(풀 크기)는 영구 슬롯 전환으로 폐기되므로 신규 설정 노브가 필요.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **per-instance TOML 설정(`config/*.toml`)에 둔다** — 예: `max_active_slots`(`DaemonInstanceConfig`, REQ-048 per-instance TOML 계열). 인스턴스별 독립 운영(다중 인스턴스 토폴로지 dcy-009 와 정합), gitignore 되는 인스턴스 파일로 운영자가 인스턴스별 동시 보유 한도를 제어. env(`TREX_*`)·하드코딩 기본값은 비채택(per-instance 일관성·기존 toml 로더 재사용).
- 반영: FR-2, §4 capacity 항목, §영향 범위(config), §10 DECIDE.
- OQ-7: 다중 인스턴스의 슬롯 영속 스코프 — 인스턴스마다 별도 DB 인가, 공유 DB + 인스턴스 스코프 컬럼인가. 영구 슬롯(FR-1)의 UNIQUE 형태·advisory lock(NFR-1)을 좌우.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **공유 DB + `instance_key` 스코프.** (1) `daily_slot` 유일성 = **`UNIQUE(instance_key, portfolio_id)`** (+ `instance_key` 컬럼), slot-scoped 테이블 전반(slot_transition_log/intent_log/ledger_outbox/snapshot_pause/killswitch_exclusion 등) instance 스코프화 + 쿼리 필터. (2) advisory lock 을 **인스턴스 스코프화**(`(SLOT_LOCK_ID, hash(instance_key))`)해 인스턴스 간 교차 경합 제거(NFR-1 재정의). (3) `daemon_id` 는 **universe 파티션(get_portfolios) + 감사용**으로만(선택 병행 저장), **스코프 키 아님**; reconcile 시 instance_key↔daemon_id **1:1 정합 가드**(불일치 표면화, 쓰기는 daemon_id 비의존). (4) cutover: 기존 단일 인스턴스 데이터에 instance_key 태깅(R-1).
- 근거: 인스턴스별 DB 는 마이그레이션/백업/관측 N배로 비합리적. 메시징이 이미 "브로커 공유 + instance_key 큐 격리"(dcy-009)라 DB 도 동일 패턴이 대칭적. **핵심: 스코프 키를 `instance_key` 로 두어 영속 스토어·락이 portfolio-hub 가용성(whoami→daemon_id)에 의존하지 않음** → 자율 안전 매도 offline 보장(REQ-024)·부팅 즉시 가용·토큰 회전에 안정. daemon_id 의 권위적 소유 이점은 1:1 가드로 흡수.
- 반영: FR-1, NFR-1, §영향 범위, R-1, §10 DECIDE. (OQ-3 의 "키=portfolio_id 단독"은 본 OQ-7 로 복합키 `(instance_key, portfolio_id)`로 정련.)
- OQ-8: 운영 중 M(`max_active_slots`)을 현재 보유 수보다 작게 변경하면(over-cap) 어떻게 처리하는가.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **초과분은 신규 활성화만 차단, 기존 보유는 자연 종료까지 유지.** M 을 낮춰 현재 holding 수 > M 이 되어도 코어는 기존 보유를 강제 청산하지 않고(자율청산 금지, issue #15), holding 수가 M 이하로 내려갈 때까지 **새 매수 활성화만 거부**(`capacity_full`)한다. 전략의 명시적 매도로 자연 수렴.
- 근거: 강제 청산은 자율청산 원칙·single-writer 안정성과 충돌. FCFS+무치환(OQ-1)과 일관 — capacity 는 "신규 진입 게이트"이지 "보유 강제 축소 트리거"가 아니다.
- 반영: FR-2(또는 인접), §10 DECIDE.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 슬롯 집합이 portfolio-hub universe 자산과 1:1 로 일치한다(universe N개 → 자산 슬롯 N개, portfolio_id 키 일치).
- S-2: 동시 활성(holding) 슬롯 수가 항상 ≤ M 이며, 후보 초과 시 OQ-1 규칙대로 결정적으로 활성 자산이 선택된다.
- S-3: 매수/매도 신호가 익명 배정 없이 해당 자산 슬롯의 활성화/비활성화로 처리되고, 단일 writer·불변식 회귀가 없다. universe↔slots 표면이 합쳐져 운영 관측이 단일화된다.
- S-4: 재정의 상태머신(FR-6: inactive↔pending_buy↔holding↔pending_sell, idle/closed 합침)·intent 자산 키 통일(FR-7: portfolio_id, 중복=`already_active`/무효=`not_holding` 거부)·`trex-core slots` 단일 뷰(FR-8)가 동작하고, over-cap 시 신규만 거부·기존 보유 유지(OQ-8)된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: universe fetch 결과 vs 슬롯 집합 대조(통합 테스트) / 데이터 출처: portfolio-hub `get_portfolios`, daily_slot
- S-2 측정 방식: 후보>M 시나리오에서 활성 수·선택 결과 단언 / 데이터 출처: 슬롯 활성 상태 + 선택 규칙
- S-3 측정 방식: 신호→활성화/비활성화 경로 단위·통합 + 동시성 경합 테스트 / 데이터 출처: 슬롯 상태 전이, 전이 로그
- S-4 측정 방식: 상태 전이 단위테스트(허용/거부 집합) + intent 멱등/거부(`already_active`/`not_holding`) 단위 + `slots` 단일 뷰 출력 단언 + over-cap(M 축소) 시나리오 / 데이터 출처: 상태머신 전이표, intent_log, slots RPC 응답
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 포지션 사이징(OQ-4 분리) + 풀 모델 폐기 범위(dcy-001/002/014/019 산출물 명시) 합의 완료(§5 범위 경계).
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~4 측정 시나리오·데이터 출처가 테스트 가능한 형태로 합의(§7).
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정 (비차단으로 이관)
- 기준: R-1(풀 모델 폐기·cutover)·R-2(완화됨) 해소 방향은 확정. **해소 책임자(owner) 지정은 사람 책임 배정이라 REQ/batch planning 단계 담당자 할당으로 이관** — Discovery confirm 비차단(confirm-discovery rule 3-4).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~8 전부 Answered, A-1 Confirmed.
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자 Justin Ko, confirm-discovery 수행(2026-06-10).
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-020_20260610_asset-centric-slot-model.md
- 참조 문서:
- docs/discovery/dcy-001_20260603_slot-pure-domain-migration.md (슬롯 도메인·상태머신 — 재해석 대상)
- docs/discovery/dcy-014_20260608_trading-date-configurable-timezone-kst.md (일일 preallocation — 폐기/재설계 대상)
- docs/discovery/dcy-019_20260609_slot-entry-count-and-closed-reallocation.md (풀 모델 슬롯 재사용 — 폐기 대상)
- docs/data-model.md, docs/runtime-flows.md, docs/project-structure.md (baseline — 전면 갱신 대상)
- GitHub issue #1(포지션 사이징), #20(중장기 자산 중심)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- [Decided 2026-06-10] 활성 후보 > M 시 규칙 (OQ-1): **FCFS + 가득 차면 거부, 코어 자율 치환 없음** (tie-break `updated_at`→`portfolio_id`). 능동 배분은 OQ-4 선언형 target 으로 분리.
- [Decided 2026-06-10] holding/수량 정본 분리 (FR-3/FR-4): **holding(포지션 열림)=코어 슬롯 로컬 정본(동기 set/clear), 수량(asset_quantity)=portfolio-hub 정본 + 로컬 best-effort 미러, 청산 수량=브로커 잔고 조회.** holding 을 hub `asset_quantity` 로 판단하지 않음(offline 안전·universe 누락 생존·hub 지연·capacity 원자성). 슬롯의 `qty`/`avg_price` 정본 소유(드리프트 원인) 정리.
- [Decided 2026-06-10] universe 변동 시 슬롯 lifecycle (OQ-2): **생성=refresh 때 즉시·멱등 / 제거=비활성 soft-retire(행 보존)·보유·pending 은 tombstone 후 닫힐 때 은퇴 / cadence=부팅+일1회(+운영자 on-demand) / degrade·대량소멸 보류 / 단일 writer / 직교 `retired_at` 마커.**
- [Decided 2026-06-10] trading_date 시맨틱 (OQ-3): **영구 슬롯(키=`portfolio_id`, `UNIQUE`, 날짜 없음)**, trading_date 는 이벤트/원장/정책의 시간 속성으로 강등, 슬롯 `trading_date` 컬럼 드롭/재의미, 일일 preallocation(dcy-014) 폐기·재해석(KST 회계 경계는 존속). cutover(일일행→영구행+보유 carry-over)는 R-1.
- [Decided 2026-06-10] 포지션 사이징 범위 (OQ-4): **분리(Out of Scope) — 본 주기 토대만.** 포지션 사이징·선언형 target intent(issue #1)는 별도 후속 Discovery. 토대 가드: 수량 가변 전제 유지 + "활성=진입/종료=전량 청산" 상태 경계 고정(HOLDING→HOLDING·부분 매도 미구현).
- [Decided 2026-06-10] holding↔hub/broker 불일치 해소 (OQ-5): **ground truth=브로커, 코어 holding=즉시 안전 구동, hub 차이=warning.** A(코어 holding/브로커 0)=error+운영자, B(코어 flat/브로커 보유, 최위험)=alert+격리+운영자(자동청산·자동편입 금지), C(수량만)=미러 갱신+warning, D(hub 지연)=기존 write-back. 감지=REQ-031 reconcile 확장, 자동은 비파괴만·실주문은 운영자 게이트. universe-detached holding(정상)=tombstone(OQ-2)과 구분.
- [Decided 2026-06-10] 동시 활성 최대치 M 설정 출처 (OQ-6): **per-instance TOML(`config/*.toml`, 예: `max_active_slots`, `DaemonInstanceConfig`/REQ-048 계열).** 인스턴스별 독립 운영, env·하드코딩 기본값 비채택. 기존 `TREX_TOTAL_SLOTS`(풀 크기)는 폐기.
- [Decided 2026-06-10] 슬롯 영속 인스턴스 스코프 (OQ-7): **공유 DB + `instance_key` 스코프.** `UNIQUE(instance_key, portfolio_id)` + slot-scoped 테이블 전반 instance 스코프화, advisory lock 인스턴스 스코프화(NFR-1 재정의), `daemon_id` 는 universe 파티션·감사용(스코프 키 아님)·instance_key↔daemon_id 1:1 가드. 인스턴스별 별도 DB 비채택. 핵심: 스코프 키를 instance_key 로 두어 영속·락이 portfolio-hub 가용성 비의존(offline 안전).
- [Decided 2026-06-10] over-cap(M 축소) 처리 (OQ-8): **초과분은 신규 활성화만 차단, 기존 보유는 자연 종료까지 유지** — 강제 청산 없음(issue #15), holding ≤ M 될 때까지 새 매수 `capacity_full` 거부.
## CONFIRM
- [Confirmed 2026-06-10] 제품 방향이 "universe 자산 중심 포지션 관리 + 동시 보유 수 제어"(익명 capacity 풀 아님)임을 확정 (A-1) — 모델 전환의 대전제. 사용자 확정.
## DATA
- 없음 (외부 데이터 의존 없음 — 내부 도메인/모델 결정)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~8 전부 Answered + 대전제 A-1 Confirmed + FR-1~8/NFR-1~5 확정 + holding/수량 정본 분리(FR-3/FR-4)·인스턴스 스코프(OQ-7)·over-cap(OQ-8) 결정. C-type placeholder 0개, Open OQ 0개, §10 DECIDE/CONFIRM 전부 해소. R-1(풀 모델 폐기·일일행→영구행 cutover)·R-2 해소 **책임자 지정은 REQ/batch planning 단계 담당자 할당으로 이관(비차단, rule 3-4)**. 승인자 Justin Ko 확정 → REQ Drafting 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-10 10:00 KST
@@ -1,221 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-021_20260610_position-sizing-target-intent
- 이슈명: 포지션 사이징 — 활성 자산 슬롯 보유 수량 변경(HOLDING→HOLDING)·부분 매도·선언형 target intent (issue #1, dcy-020 OQ-4 후속)
- 작성 시각(KST): 2026-06-10 17:06 KST
- 마지막 갱신 시각(KST): 2026-07-29 19:43:47 KST (후속 자본배분 정책 Discovery 참조 연결)
- 대체됨: 없음
- 후속 Discovery 참조: `docs/discovery/dcy-042_20260729_shared-account-capital-allocation-policy.md`
- 생성된 REQ 참조: REQ-066, REQ-067 (+ change-req REQ-057/061/062)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #1(포지션 사이징/선언형 target intent). dcy-020(자산 중심 영구 슬롯 — 본 주기의 직접 토대, OQ-4 에서 분리), dcy-013(부분 체결 후 외부 취소 — 부분 수량/잔량 처리 선례), REQ-062(청산 수량 브로커 정본·수량 hub 정본+미러), REQ-057(상태머신).
- 해석:
- dcy-020 이 자산 중심 영구 슬롯 substrate 를 완성하면서 "수량 가변 전제는 모델에 남김 / 활성=진입·종료=전량 청산 경계는 고정"으로 OQ-4(포지션 사이징)를 명시 분리했다. 본 Discovery 는 그 토대 위에 **보유 중 수량 조정(증액·부분 감액)과 선언형 target intent** 를 얹는 후속 주기다.
- 핵심 판단: HOLDING 상태에서 같은 자산 슬롯의 수량을 바꾸는 상태머신·집행·intent 계약을 정의한다. **계약 형태는 명령형으로 확정**(OQ-1, 2026-06-10): 전략이 스냅샷의 cash+quantity 로 사이징을 계산해 qty+price 를 실은 매수(증액)/부분 매도 intent 를 보내고, core 는 실행 substrate(단일 writer·멱등·브로커 세션·슬롯 상태)에 집중한다. core 는 "의도 수량"을 저장/추적하지 않으며, 드리프트 재수렴은 전략이 스냅샷으로 수행한다. 남은 판단은 단위(OQ-2)·avg_price 가중평균 세부(OQ-3)다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-020 은 OQ-4/issue #1 을 "별도 후속 Discovery"로 명시 분리했고 본 주기와 문제 정의(보유 수량 가변)·성공 기준이 다르다 — dcy-020 갱신이 아니라 신규 주기다.
- 본 변경은 dcy-020 이 고정한 상태머신 경계("종료=전량 청산", HOLDING→HOLDING 미포함)를 해제하므로 dcy-020 의 설계 전제를 확장하는 독립 주기로 추적해야 책임 경계가 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/domain/slot/state.py` 의 상태머신은 이산적이다: `inactive → pending_buy → holding → pending_sell → inactive`(+error). **HOLDING→HOLDING(수량 변경) 전이가 없고**, 매도는 전량 청산(pending_sell→inactive)만 의도적으로 표현된다. 부분 수량 잔량 처리는 `execution.py` 의 외부 취소 경로(dcy-013, `_on_partial_cancel`: pending_sell→holding + qty 차감)에만 존재하며 이는 반응적 처리이지 의도적 부분 매도 intent 가 아니다.
- `application/intents.py` 의 `BuyIntent` 는 inactive 슬롯을 활성화(qty 지정)할 뿐, 이미 holding 인 슬롯의 수량을 늘리는(증액) 경로가 없다. `SellIntent` 는 전량 청산만 한다(부분 수량 인자 없음).
- 수량 정본은 브로커 잔고(REQ-062, `BrokerPort.get_balance().hldg_qty`)이고 슬롯 `qty` 는 미러다. avg_price 는 매수 체결 시 1회 설정되고 증액 시 가중평균 재계산 경로가 없다.
- dcy-020 가 "수량 가변 전제"를 모델에 남겨 둔 토대(영구 슬롯·자산 1:1)는 이미 존재한다 — substrate 재작성은 불필요하다.
- 기대 상태
- 보유 중인 자산 슬롯이 `holding` 을 유지한 채 목표 수량으로 **증액(추가 매수)·부분 감액(부분 매도)** 될 수 있다. 전량 청산만 inactive 로 종료한다.
- 전략 모듈이 자산의 **목표 포지션을 선언(target)** 하거나 증감을 지시하면, core 가 현재 보유(브로커 정본)와 비교해 필요한 매수/매도 수량을 도출·집행한다. avg_price 는 증액 시 가중평균으로 재계산되고 부분 매도 시 cost basis 가 유지된다.
- 이해관계자
- 전략 모듈(trex-orb) 개발자 — target/사이징 intent 를 발행하는 주체
- trex-core 유지보수자 — 상태머신·집행·intent 계약 확장 구현
- 운영자 — 슬롯의 수량 변화·부분 매도를 `trex-core slots`/이벤트로 관측
- 제약사항
- 집행 매도 수량은 브로커 잔고 정본(REQ-062)을 유지해야 한다 — 부분 매도도 과매도/이중 주문이 없어야 한다.
- dcy-020 의 자산 1:1 영구 슬롯·M(동시 활성 자산 수) 캡·instance 스코프 불변식을 깨지 않는 additive 확장이어야 한다.
- 단일 writer(SlotService advisory lock)·intent 멱등(intent_log) 불변식 유지.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략이 자산 A 를 100주 보유 중인데 신호 강화로 목표를 150주로 올린다 → 같은 슬롯이 holding 을 유지하며 50주 추가 매수, avg_price 가중평균 재계산.
- 전략이 자산 A 목표를 100→40주로 낮춘다 → 같은 슬롯이 60주 부분 매도 후 holding(잔량 40) 유지, cost basis 유지. 목표 0 이면 전량 청산 → inactive.
- 운영자가 `trex-core slots --symbol …` 로 부분 매도 진행/잔량을 관측하고, 부분 체결 이벤트를 전략이 수신해 목표 대비 현재를 재조정.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 보유 자산 슬롯의 **수량 조정 상태 전이** 도입: 증액(HOLDING→PENDING_BUY→HOLDING, qty 누적·avg_price 가중평균)과 부분 감액(HOLDING→PENDING_SELL→HOLDING 잔량>0 / →INACTIVE 전량)을 의도적 intent 경로로 표현한다.
- **포지션 사이징 intent 계약**(명령형, OQ-1 확정): 전략이 cash+quantity(스냅샷)로 계산한 qty+price 를 실은 매수(증액)/부분 매도 intent 를 보내면 core 가 단일 writer·멱등(intent_log) 하에 실행한다. core 는 target/의도 수량을 저장·추적하지 않는다(미체결 잔량 무추적). 집행 매도 수량은 `min(intent qty, 브로커 잔고)`(과매도 방지), intent 가 잔고 이상이면 전량 매도.
- baseline 문서 갱신: `docs/data-model.md`(슬롯 수량 가변·avg_price 재계산 규칙), `docs/runtime-flows.md`(사이징 intent→reconcile→집행 흐름, 상태머신 전이 확장), `docs/interface-contract.md`(사이징 intent payload·이벤트), `docs/project-structure.md`(영향 모듈) 를 실제 구현과 일치시킨다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/domain/slot/state.py`: HOLDING→PENDING_BUY(증액) 전이 추가, 부분 매도 후 HOLDING 복귀(기존 pending_sell→holding 재사용) 의미 정립.
- `trex_core/domain/slot/service.py`: 증액 시 qty 누적 + avg_price 가중평균 재계산, 부분 매도 시 잔량 계산·cost basis 유지 필드 규칙.
- `trex_core/application/intents.py`: 사이징/target intent(증액·부분 감액) admission 경로 + 멱등.
- `trex_core/application/execution.py`: 증액 매수 dispatch, 부분 매도 수량(브로커 정본 기반) dispatch — dcy-013 `_on_partial_cancel`/REQ-062 와 정합.
- `trex_core/application/messaging_handlers.py`: 사이징 intent payload 파싱 + 이벤트.
- 문서 영향:
- `docs/data-model.md`, `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/project-structure.md` 갱신.
- 운영 영향:
- 전략 모듈(trex-orb)이 새 사이징 intent 를 발행하도록 계약 전환 필요(점진/하위호환 검토).
- 부분 매도·증액 주문이 늘어 KIS 주문 빈도·체결 모니터링 영향.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 보유(holding) 자산 슬롯의 **수량 증액**을 지원한다. 전략이 qty+price 를 실은 매수 intent 를 보내면 같은 슬롯이 HOLDING→PENDING_BUY→HOLDING 로 순환하며 **실제 체결분만큼** 보유 수량이 누적되고, avg_price 는 (기존수량×기존평단 + 체결수량×체결가)/총수량 가중평균으로 재계산된다. 부분 체결 시 실제 체결분만 반영·ledger 기록하고 미체결 잔량은 추적하지 않는다(재수렴은 전략). 활성 자산 수(M 캡 대상)는 증가하지 않는다.
- FR-2: 보유 자산 슬롯의 **부분 감액(부분 매도)**을 지원한다. 전략이 qty+price 를 실은 (부분)매도 intent 를 보내면 같은 슬롯이 HOLDING→PENDING_SELL 로 전이한다. 집행 매도 수량 = **`min(intent qty, 브로커 hldg_qty)`**(REQ-062 과매도 방지); intent 가 잔고 **이상**이면 전량 매도. 체결 후 슬롯은 브로커 정본 수량을 반영해 잔량>0 이면 HOLDING(cost basis 유지), 0이면 INACTIVE 로 종료한다. **부분 체결 시 실제 체결분만 ledger 기록(REQ-018/019)**, 미체결 잔량은 core 가 추적·재시도하지 않고, 더 줄이려면 전략이 다음 스냅샷 기준으로 재발행한다(dcy-013 부분 체결 처리 재사용).
- FR-3: **포지션 사이징 intent 계약 = 명령형**(OQ-1 확정). 전략이 스냅샷의 cash·asset_quantity(REQ-028/029, option D)로 사이징을 계산해 **qty+price 를 실은** 매수(증액)/부분 매도 intent 를 발행하고, core 는 단일 writer·멱등(intent_log) 하에 실행만 한다. **wire 계약 단위 = 정수 주식 수(qty) + 가격(price)**(OQ-2 확정); 비중(weight)/금액(capital) 사이징의 주식 수 환산과 반올림·lot 처리는 **전략 책임**(core 는 정수 qty 를 그대로 집행, 현재가·cash 환산 로직 없음). core 는 target/의도 수량을 **저장·diff·추적하지 않으며**(reconcile-on-intent 없음), 실현손익은 ledger/portfolio-hub 가 소유한다(core 는 실제 체결만 원장 기록). 드리프트·유실 복구는 전략이 스냅샷(현재 수량 = 브로커 정본 표면화)으로 재수렴한다. 기존 buy/sell intent 와 병존하는 additive 확장이다.
- FR-4: baseline 문서(`docs/data-model.md`, `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/project-structure.md`)를 수량 가변 슬롯·사이징 흐름·상태머신 전이 확장·intent/이벤트 계약에 맞게 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 부분 매도·증액은 dcy-020/REQ-062 의 "수량=브로커 정본, 슬롯=미러" 불변식과 단일 writer·집행 멱등을 유지한다 — 과매도·이중 주문·avg_price drift 가 없어야 한다.
- NFR-2: 상태머신·intent·집행 변경은 dcy-020 자산 중심 모델에 대한 **additive 확장**이어야 한다(기존 활성/청산/admission 경로 무회귀). 기존 전 단위 회귀 green 유지.
- NFR-3: 사이징 intent 는 멱등(intent_id) + 이벤트로 부분 체결/증액 결과를 전략에 last-wins 로 전달해, 전략이 목표 대비 현재를 재수렴할 수 있어야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 보유 자산 슬롯의 증액·부분 감액 상태 전이·집행·avg_price 재계산
- 포지션 사이징/target intent 계약(형태는 OQ-1 확정) + 멱등 + 이벤트
- 관련 baseline 4문서 갱신
- Out of Scope
- 능동 capacity 배분(M 슬롯을 어떤 자산에 줄지 core 가 최적 선택) — dcy-020 FR-2 에서 분리된 별개 주제, 본 주기는 "이미 보유한 자산의 수량 조정"에 한정(2026-06-10 확정)
- 자동 리밸런싱 스케줄러(주기적 목표 재산정) — 전략 모듈 책임
- 비중(weight)/금액(capital)→주식 수 환산·반올림·lot 처리 (OQ-2 확정: **전략 책임**, core 범위 밖 — wire 는 정수 주식 수+가격)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 집행 핫패스 변경(부분 매도·증액 주문)으로 과매도·이중 주문·avg_price 오류 위험. 라이브 주문 동작.
- 완화 방안: 매도 수량 브로커 정본(REQ-062) 재사용, 멱등(intent_log)+dispatch 가드, dcy-013 부분 체결 처리와 정합, 단위+paper 리허설+run-the-app 검증.
- R-2 (High): dcy-020 이 고정한 상태머신 경계("종료=전량 청산", HOLDING→HOLDING 미포함) 해제로 기존 불변식·테스트가 흔들릴 위험.
- 완화 방안: 전이는 additive 추가(기존 전이 보존), 5→확장 상태머신 회귀 가드, ALLOWED_TRANSITIONS 명세 테스트 갱신.
- R-3 (Medium): 전략 모듈(trex-orb) 동시 계약 변경 필요 — 미전환 전략과의 호환.
- 완화 방안: 기존 전량 매수/매도 intent 와 병존(additive), 사이징 intent 는 신규 경로로 도입해 점진 전환.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-020 자산 중심 영구 슬롯(자산 1:1·수량 가변 전제 유지)이 토대이므로 substrate 재작성 없이 additive 확장으로 충분하다.
- A-2: 보유 수량 정본은 브로커 잔고(REQ-062)이고 슬롯 `qty` 는 미러다 — 사이징도 이 정본/미러 분리를 그대로 따른다.
- A-3: M 캡은 동시 활성 자산 수 제어이므로, 이미 활성인 자산의 증액은 active 수를 늘리지 않아 M 캡과 무관하다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 사이징 intent 계약 형태 — 선언형 target vs 명령형 delta vs 혼합.
- 상태: **Answered (2026-06-10, 사용자 결정)**
- 결정: **명령형** — 전략이 스냅샷의 cash+quantity 로 사이징(qty+price)을 계산해 매수(증액)/부분 매도 intent 를 보내고, core 는 실행 substrate(target/의도 수량 미저장·미추적). 근거: ① 멱등성은 intent_log dedup 이 이미 수량 레벨로 보장(REQ-007/023), ② 주문에 가격(지정가)이 따라가야 함(dcy-012), ③ cash 정본은 portfolio-hub 라 사이징 계산은 전략 책임(관심사 분리), ④ 기존 명령형 buy/sell 계약의 최소 additive 확장. 부수 결정: 과매도→`min(intent,잔고)`·이상이면 전량(FR-2), 부분 체결→실제 체결만 ledger·미추적·전략 재수렴(FR-1/FR-2). 반영: FR-1/FR-2/FR-3, §4.
- 종료 조건: 충족(FR-3 계약 형태 확정).
- OQ-2: 사이징 단위 — 절대 수량 vs 비중 vs 금액 + 환산 책임.
- 상태: **Answered (2026-06-10, 사용자 결정)**
- 결정: **wire 단위 = 정수 주식 수(qty) + 가격(price)**. 비중/금액→주식 수 환산·반올림·lot 처리는 **전략 책임**(core 는 정수 qty 를 그대로 집행). 근거: ① OQ-1 명령형·기존 buy/sell·KIS 주문이 모두 주식 수+가격이라 새 단위 불필요(최소 additive), ② 환산에 필요한 cash(정본=portfolio-hub)·주문 가격을 전략이 이미 쥐고 있어 환산-주문 가격 일관, ③ 시장 미시구조(lot/호가) 규칙을 core 에서 배제. 주의: 스냅샷 cash 는 미정산 시 추정값(`cash_estimated`, dcy-018)일 수 있어 금액/비중 사이징 오차창은 전략이 감안. 반영: FR-3, §5 Out of Scope, §10 DATA.
- 종료 조건: 충족(정본 단위·환산 책임 확정).
- OQ-3: avg_price 재계산·cost basis 정책.
- 상태: **Answered (2026-06-10, 사용자 결정)**
- 결정: 증액 시 avg_price = **`(기존수량×기존평단 + 체결수량×체결가)/총수량`** 가중평균(FR-1). 부분 체결 시 **실제 체결분만** 누적(미체결 무시)이라 자연 정합. 부분 매도 후 잔량 avg_price 는 **변경 없음(cost basis 유지, dcy-013 일관)**. 집행 매도 수량 = `min(intent,잔고)`(과매도 방지), 실현손익은 **ledger/portfolio-hub 소유**(core 미계산, 실제 체결만 원장 기록). 반영: FR-1/FR-2/FR-3.
- 종료 조건: 충족(avg_price·cost basis·실현손익 규칙 확정).
- OQ-4: 사이징 reconcile 주기·트리거.
- 상태: **Answered (2026-06-10, 사용자 결정)**
- 결정: **intent 수신 시 1회 집행**. OQ-1 명령형 확정으로 core 는 target 을 저장하거나 주기적으로 reconcile 하지 않는다(reconcile-on-intent 없음). 드리프트·유실 재수렴은 전략이 스냅샷(현재 수량=브로커 정본)으로 수행하고, **자동 리밸런싱 스케줄러는 Out of Scope(전략 책임)**. 반영: FR-3, §5 Out of Scope.
- 종료 조건: 충족(트리거 모델 In/Out of Scope 확정).
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 보유 자산의 목표 상향 시 같은 슬롯이 holding 을 유지하며 추가 매수로 수량이 증가하고 avg_price 가 가중평균으로 재계산된다.
- S-2: 목표 하향 시 같은 슬롯이 부분 매도 후 holding(잔량>0, cost basis 유지) 또는 전량 시 inactive 로 종료되며, 집행 매도 수량이 브로커 잔고 정본과 일치한다(과매도 0).
- S-3: 사이징 변경 전·후 dcy-020 기존 경로(활성/청산/admission/M 캡/instance 스코프) 회귀가 없고, baseline 4문서가 구현과 일치한다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 단위 테스트(증액 전이·avg_price 가중평균) + run-the-app(증액 intent→holding qty 증가) / 데이터 출처: `tests/`, daily_slot, slot_transition_log
- S-2 측정 방식: 단위+PG 테스트(부분 매도 잔량/전량 분기) + paper 집행 매도 수량==브로커 잔고 검증 / 데이터 출처: `tests/`, BrokerPort.get_balance, ledger_outbox
- S-3 측정 방식: 전 단위 회귀 green + ruff + baseline 문서 diff 리뷰 / 데이터 출처: `pytest`, `docs/*.md`
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정 — capacity 배분 Out of Scope·단위(OQ-2) 확정으로 In/Out 갈림 없음
- [x] 성공 지표의 측정 방식/데이터 출처 확정 — S-1~S-3 모두 기존 pytest·PG·run-the-app 경로로 측정 가능(§7)
- [x] High 영향 리스크 검증 방식 확정(R-1 집행=브로커 정본+멱등+리허설, R-2 상태머신=additive+회귀 가드, §6) — 해소 **책임자 지정은 REQ/batch planning 단계로 위임(비차단)**
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 — OQ-1~4 전부 Answered (2026-06-10)
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
- [x] Freeze 확정 담당자(Confirmed By) 지정 — Justin Ko (§11)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-021_20260610_position-sizing-target-intent.md
- 참조 문서:
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md (토대·OQ-4 분리 출처)
- docs/discovery/dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md (부분 체결/잔량 처리 선례)
- stage-pilot/templates/project-structure.md, stage-pilot/templates/runtime-flows.md (baseline 갱신 논리 템플릿 참조)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (해소) 사이징 단위 = 정수 주식 수+가격, 환산은 전략 책임 — OQ-2 확정, FR-3/§5 반영.
- (해소) 능동 capacity 배분(M 슬롯 자산 선택)은 **Out of Scope** — dcy-020 FR-2 에서 분리된 별개 주제, 본 주기는 "이미 보유한 자산의 수량 조정"에 한정. §5 Out of Scope 반영.
- (해소) 사이징 reconcile 트리거 = intent 당 1회(core 스케줄 없음) — OQ-4 확정, FR-3/§5 반영.
## CONFIRM
- (해소) avg_price 정책 = 증액 가중평균 + 부분 매도 cost basis 유지 + 실현손익 ledger/hub 소유 — OQ-3 확정, FR-1/FR-2/FR-3 반영.
- (해소) trex-orb 사이징 intent 전환 = 기존 buy/sell 와 **병존(additive·점진 전환)** — OQ-1 확정, FR-3/R-3 반영.
## DATA
- (해소) 전략 신호 표현 방식(목표/증감/비중)은 OQ-1/2 확정으로 **core 범위 밖** — wire 는 항상 정수 주식 수+가격이라 core 는 전략 내부 단위를 알 필요 없음. trex-orb 측 환산 구현은 trex-orb Discovery 에서 다룸.
> 참고: 미체결 pending 고착 처분·운영자 강제 취소(issue #21)는 본 주기와 다른 문제(PENDING 처분/안전 통제)로 **별도 Discovery 후보**다 — dcy-021 범위 밖.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1(명령형 계약)·OQ-2(정수 주식 수+가격, 환산 전략)·OQ-3(avg_price 가중평균·cost basis·실현손익 hub)·OQ-4(intent 당 1회, 스케줄 없음) 전부 Answered 로 핵심 결정 확정. capacity 배분 경계(Out of Scope)·단위·avg_price·트리거 전부 확정. confirm-discovery 승인 완료(Justin Ko).
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-10 17:39 KST
@@ -1,220 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-022_20260610_stuck-pending-order-recovery
- 이슈명: 전략 발신 intent 실행 정책 — 주문 timeout(TIF)/슬리피지 허용 + 미체결 고착 백스톱 (issue #21, dcy-020/021 모델 전제)
- 작성 시각(KST): 2026-06-10 17:43 KST
- 마지막 갱신 시각(KST): 2026-06-10 19:30 KST (confirm-discovery 승인 — OQ-1~4 Answered, 저장소 사실 재확인(cancel_order 미배선·payload 무 timeout/slippage), C형 0 → 상태 confirmed)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 없음
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #21(pending 고착). dcy-020(자산 중심 영구 슬롯·slot_id 정본·상태머신 — 토대, BAT-027~030 delivery), dcy-021(포지션 사이징·부분 매도 — BAT-031), dcy-013(stale 워치독 Alert-only + 외부취소 cncl_yn 롤백 선례).
- 해석:
- 본래 issue #21 은 "미체결 고착을 운영자/코어가 사후 정리"하는 반응적 문제였다. 그러나 더 나은 근원 해법은 **전략이 intent 제출 시 실행 정책(주문 timeout, 슬리피지 허용)을 함께 실어** core 가 이를 집행하는 것이다 — 미체결 주문은 전략이 지정한 timeout 에 자동 취소되어 애초에 고착되지 않는다.
- 따라서 본 Discovery 의 **주축**은 intent 실행 정책(timeout=TIF + slippage)이고, 운영자 강제 취소·코어 fallback 은 **백스톱**으로 둔다. 가격(`entry_price`)·청산시간(`forced_close_at`)·수량(`qty`)은 이미 계약에 있으므로 **신규는 timeout 과 slippage 두 가지**다. 브로커 `cancel_order` 는 어댑터에 이미 존재(미배선)라 timeout 집행 배선만 필요하다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-013(REQ-014)은 stale pending 을 **Alert-only**(상태 변경 없음)로 의도 고정했다. 본 주기는 전략이 명시한 실행 정책(timeout)을 core 가 집행하므로 "코어 임의 취소" 우려가 해소되며, 능동적 취소·실행 정책을 정의하는 신규 주기다.
- intent 에 실행 정책(timeout/slippage)을 추가하는 것은 매수/매도 계약(REQ-007/021/061)의 확장이며, 기존 dcy 어디에도 정의되지 않은 신규 능력이다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **주문에 실행 정책이 없다**: buy/sell intent payload(`interface-contract.md`)는 `entry_price`(지정가)·`qty`·`forced_close_at`(청산시간)·`stop_loss_price`/`take_profit_price` 는 있으나, **주문 유효시간(TIF/timeout)·슬리피지 허용**을 지정할 수단이 없다. 지정가 미체결 시 무기한 대기.
- **미체결 시 무기한 pending**: 집행 루프(`execution.py`)는 poll 결과가 "접수/부분(취소 아님)"이면 상태를 두고 재polling 한다 — 체결·외부취소·거부가 없으면 자산 슬롯이 pending_buy/pending_sell 에 영구 고착(issue #21).
- **stale 워치독은 Alert-only**: `_check_stale_pending`(dcy-013/REQ-014)은 임계 초과 시 WARN+카운터만 — 자동 조치 없음. 의도는 "코어가 전략 의도를 몰라 임의 취소 불가"였다.
- **브로커 취소 송신 미배선**: 어댑터에 `cancel_order(...)`가 이미 존재(`paper.py`/`kis.py`)하나 어떤 자동/운영자 흐름에도 호출되지 않는다.
- **운영자 처분 경로 부재**: `trex-core exit` 는 holding 대상이라 pending 에 적용 불가.
- **(dcy-021 후) 부분 매도 고착**: 포지션 사이징 부분 매도 주문도 pending_sell 로 나가 미체결 시 같은 고착 + 보유 슬롯 조정 잠금.
- baseline(`docs/interface-contract.md` payload, `docs/runtime-flows.md` 집행) 갱신 대상.
- 기대 상태
- 전략이 intent 에 `timeout_sec`(예 60)·`slippage_pct`(예 0.1%)를 실으면, core 가 이를 집행한다 — timeout 초과 미체결 주문은 자동 취소·롤백(고착 예방), slippage 로 보호 지정가를 산출해 과도한 불리 체결을 막는다.
- timeout 이 없는(또는 timeout 취소 자체가 실패한) 잔여 고착은 운영자 강제 취소 + (선택) 코어 fallback 으로 처리한다(백스톱).
- 가격·청산시간·수량과 함께 실행 정책이 하나의 주문 명세로 통합되어, 전략이 "어떻게 체결할지"를 선언한다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물
- 리뷰어/승인자 — 실행 정책 계약·취소 집행·안전성 승인
- 전략 모듈(orb-v1 등) — intent 에 timeout/slippage 발신(외부 repo 발신 측 변경)
- 운영 담당자 — 잔여 고착 백스톱 처분·관측
- 제약사항
- 취소·롤백은 단일 writer·멱등·불변식(advisory lock, instance 스코프 dcy-020)을 보존하고, 이미 체결된 부분 수량을 잃지 않아야 한다(dcy-013 부분 취소 회계 계승).
- 취소 요청 송신과 실제 취소 확인(cncl_yn/poll)을 분리 — 취소-체결 race 처리.
- 슬리피지·timeout 미지정 시 기존 동작(무기한 지정가/시장가)과 호환(하위호환·옵션 필드).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- orb-v1 이 매수 신호를 줄 때 `entry_price`·`qty`·`forced_close_at` 과 함께 `timeout_sec=60`·`slippage_pct=0.1` 을 실어 보낸다. 60초 내 미체결이면 core 가 주문을 취소하고 슬롯을 inactive 로 롤백한다(고착 없음).
- 시장가 매수에 `slippage_pct=0.1` 을 주면 core 가 현재가×(1+0.1%) 보호 지정가로 제출해 과도한 불리 체결을 막는다. 밴드 밖이면 미체결 → timeout 취소로 수렴.
- 부분 매도(dcy-021)에 timeout 을 주면 미체결 시 자동 취소되어 보유로 되돌아가고 부분 체결분은 보존된다.
- (백스톱) timeout 없는 옛 주문이 고착되면 운영자가 `trex-core cancel <slot_id>` 로 취소·롤백한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- **intent 실행 정책 필드 추가(주축)**: buy/sell payload 에 optional `timeout_sec`(주문 유효시간, TIF)·`slippage_pct`(슬리피지 허용) 추가. 계약(REQ-007/021/061) 확장. 운영자 config 기본 timeout(per-instance TOML `default_order_timeout_sec`, opt-in) + per-intent override(OQ-4). deprecate 없음.
- **timeout 집행**: 집행 루프가 dispatched-pending 주문의 경과시간을 추적해 `timeout_sec` 초과 시 브로커 `cancel_order` 송신 → 취소 확인 후 롤백(pending_buy→inactive / pending_sell→holding, 부분 체결 보존). 기존 어댑터 `cancel_order` 배선.
- **슬리피지 집행(OQ-2/3)**: `slippage_pct` 는 `entry_price` 기준 밴드 — 실효 지정가 = 매수 `entry_price*(1+pct)`/매도 `entry_price*(1-pct)`(호가단위 반올림). `entry_price` 없으면 무시(순수 시장가). 즉 `entry_price`=기준/목표, `slippage_pct`=허용폭, 실효 limit=둘의 곱(`entry_price` 단독이면 하드 지정가=현행).
- **고착 백스톱**: timeout 미지정/취소 실패 잔여 고착에 운영자 강제 취소(`cancel`) + (선택) 코어 fallback. dcy-013 Alert-only 는 기본 유지.
- baseline 문서(`docs/interface-contract.md` payload, `docs/runtime-flows.md` 집행/취소 흐름) 반영.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/intents.py` / `messaging_handlers.py` — payload `timeout_sec`/`slippage_pct` 파싱·운반, 슬롯/주문에 전달
- `trex_core/application/execution.py` — dispatched-pending 경과시간 추적 + timeout 취소 송신, slippage→limit_price 산출, 취소 확인 후 롤백
- `trex_core/adapters/broker/*` — 기존 `cancel_order` 배선(신규 메서드 아님), 보호 지정가 주문
- `trex_core/domain/slot/service.py` / `models.py` — (필요 시) 주문 정책/제출시각 보유 + 취소 롤백 전이(부분 보존)
- `trex_core/runtime/cli.py` / control RPC — 운영자 `cancel` 백스톱
- `trex_core/runtime/config.py`(`DaemonInstanceConfig`) + `config/*.toml` — `default_order_timeout_sec` opt-in 기본값(REQ-048 per-instance TOML 계열, OQ-4)
- 문서 영향:
- `docs/interface-contract.md`(buy/sell payload 에 timeout/slippage), `docs/runtime-flows.md`(집행·timeout 취소·slippage·백스톱)
- 운영 영향:
- 미체결 고착 예방으로 활성 capacity 회복. 전략 모듈(외부) 발신 계약 변경 조율 필요.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: buy/sell intent payload 에 optional `timeout_sec`(주문 유효시간)·`slippage_pct`(슬리피지 허용)를 추가하고 core 가 파싱·운반한다(REQ-007/021/061 확장). core 는 수신 측 먼저 배포 가능(필드 누락 시 무시 — orb-v1 발신과 lockstep 불필요, 하위호환). `timeout_sec` 미지정 시 운영자 config 기본값(per-instance TOML `default_order_timeout_sec`, 미설정=OFF=무기한)을 적용하고 per-intent 값이 override 한다(OQ-4). `slippage_pct` 는 기본값 없음(미지정=보호밴드 없음).
- FR-2: 집행 루프는 dispatched-pending 주문이 `timeout_sec` 를 초과하면 브로커 `cancel_order` 를 송신하고, 취소 확인 후 슬롯을 롤백한다(pending_buy→inactive / pending_sell→holding). 이미 체결된 부분 수량은 보존한다.
- FR-3: `slippage_pct` 는 **기준가(`entry_price`)가 있을 때만** 적용한다(OQ-2) — 실효 지정가 = 매수 `entry_price*(1+pct)` / 매도 `entry_price*(1-pct)`(KRX 호가단위로 반올림: 체결 보장 측 — 매수 올림/매도 내림). `entry_price` 가 없으면(순수 시장가) `slippage_pct` 는 **무시**(무제한 시장가). 밴드 밖 미체결은 timeout 취소(FR-2)로 수렴한다.
- FR-4: 취소 요청 송신과 실제 취소 확인(cncl_yn/poll)을 분리한다 — 취소가 거부(이미 체결)되면 정상 체결 경로로 수렴하고 슬롯을 임의 변경하지 않는다(취소-체결 race).
- FR-5: (백스톱) 운영자가 자산 슬롯의 고착 pending 을 강제 취소·롤백할 수 있다(timeout 미지정/취소 실패 대비). dcy-013 Alert-only 기본 동작은 회귀 없이 유지한다.
- FR-6: `docs/interface-contract.md`·`docs/runtime-flows.md` 에 실행 정책·timeout 취소·slippage·백스톱을 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 안전·멱등 — 취소·롤백은 단일 writer·advisory lock(instance 스코프)·멱등을 보존하고, 중복 취소/경합에서 부분 체결분을 잃지 않는다.
- NFR-2: 비차단 — timeout 취소 송신 실패·지연이 집행 루프·다른 슬롯 처리를 막지 않는다(best-effort, 요청/확인 분리).
- NFR-3: 하위호환 — `timeout_sec`/`slippage_pct` 미지정 시 기존 매수/매도 경로와 byte 동등(회귀 없음).
- NFR-4: 관측성 — timeout 취소·slippage 적용·백스톱 처분이 로그+카운터로 가시화된다(stale 카운터 계승).
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- intent `timeout_sec`/`slippage_pct` 계약 + core 집행(timeout 취소·보호 지정가)
- 브로커 `cancel_order` 배선 + 취소-체결 race 처리(부분 보존)
- 운영자 강제 취소 백스톱 + runtime-flows/interface-contract 정합
- Out of Scope
- 브로커 `cancel_order` 신규 구현(이미 존재 — 배선만)
- 전략 모듈(orb-v1) 내부 신호/사이징 로직(외부 repo, 발신 계약만 합의)
- IOC/FOK 등 네이티브 TIF 분류 확장 — **후속 주기**(본 주기는 코어-타이밍 `timeout_sec` 만, OQ-1 Answered)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): 취소 요청과 실제 체결의 경쟁(race) — timeout 취소 송신 직전/직후 체결되면 슬롯과 브로커 실제가 어긋날 수 있다.
- 완화 방안: 취소 송신 후 즉시 롤백하지 말고 cncl_yn/poll 확인 후 전이(FR-4). 이미 체결 시 정상 경로로 수렴.
- R-2 (Medium): 슬리피지 보호 지정가 산출 오류로 의도와 다른 체결/미체결.
- 완화 방안: 방향별 부호·산출식을 명확히 정의(OQ-2), 단위 테스트. 밴드 밖은 timeout 취소로 안전 수렴.
- R-3 (Medium): 부분 체결분 처리 오류로 회계/포지션 불일치.
- 완화 방안: dcy-013 부분 취소 회계 선례 재사용 + 브로커 정본 대조(REQ-062) + 테스트.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-020(자산 중심 영구 슬롯·slot_id 정본)·dcy-021(부분 매도)이 구현 완료된 상태다(BAT-027~031).
- A-2: 브로커 어댑터 `cancel_order` 가 동작하며 cncl_yn 으로 취소 결과를 관측 가능하다(dcy-013).
- A-3: 전략 모듈이 intent 에 실행 정책 필드를 발신하도록 합의·변경 가능하다(외부 repo).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: timeout 시맨틱 범위 — 단순 `timeout_sec`(미체결 시 취소)만 우선 도입하고 IOC/FOK 등 TIF 분류는 후속으로 둘지, 본 주기에 함께 정의할지.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: **timeout 만 도입** — `timeout_sec`(N초 미체결 시 코어가 `cancel_order` 송신·롤백, GTD/timeout 성격, 브로커 무관). **IOC/FOK 등 네이티브 TIF 는 후속 주기**(브로커 네이티브 플래그·부분 체결 규칙 별도). 반영: §4, FR-1/2, §5 범위 경계.
- OQ-2: 슬리피지 산출 규칙.
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: `slippage_pct` 는 **기준가 `entry_price` 가 있을 때만** 적용 — 실효 지정가 = 매수 `entry_price*(1+pct)` / 매도 `entry_price*(1-pct)`(KRX 호가단위 반올림, 체결 보장 측). `entry_price` 없으면(순수 시장가) **무시**(무제한). 시장가엔 곱할 기준이 없다는 점에서 "기준가 없으면 슬리피지 없음". 반영: FR-3, §4.
- OQ-3: 필드 상호작용/우선순위.
- 상태: Answered (2026-06-10, OQ-2 동반 결정)
- 결정: `entry_price`=기준/목표, `slippage_pct`=허용폭 → 실효 limit = `entry_price*(1±pct)`. `entry_price` 단독(slippage 없음)=하드 지정가(현행). `timeout_sec`=주문 유효시간(미체결 취소), `forced_close_at`=포지션 만기 청산(별개 축, REQ-066/037). 네 필드 역할 분리 명확. 반영: FR-3, §4, payload 계약(REQ 단계 interface-contract).
- OQ-4: 계약/발신 위치 — `timeout_sec`/`slippage_pct` 를 buy/sell payload optional 로 추가(core 수신)하고, 전략 모듈(orb-v1) 발신 변경을 어떻게 조율할지(기본값·하위호환·deprecate 정책).
- 상태: Answered (2026-06-10, 사용자 결정)
- 결정: (1) buy/sell payload 에 **optional** 필드 추가 — core 수신 측 먼저 배포(누락 시 무시), orb-v1 발신은 별도 일정(lockstep 불필요·하위호환). (2) `timeout_sec` 기본값 = **운영자 config opt-in**(per-instance TOML `default_order_timeout_sec`, 미설정=OFF=현행), per-intent override. `slippage_pct` 기본값 없음. (3) **deprecate 없음**(전부 신규 optional 필드, `entry_price` 등 기존 필드 유지). 반영: FR-1, §4, 영향 범위(config).
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: `timeout_sec=N` 을 실은 intent 가 N초 내 미체결이면 core 가 주문을 취소하고 슬롯을 롤백하며 부분 체결분을 보존한다(고착 없음).
- S-2: `slippage_pct` 가 주어진 주문이 방향별 보호 지정가로 제출되고, 밴드 밖이면 미체결 → timeout 취소로 수렴한다. 미지정 주문은 기존 동작과 동일(회귀 없음).
- S-3: 취소-체결 race 에서 이미 체결된 경우 정상 체결 경로로 수렴(임의 롤백 없음). 백스톱 운영자 `cancel` 로 timeout 없는 고착도 해소된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: timeout 경과 픽스처 → 취소·롤백·부분 보존 단위/통합 / 데이터 출처: 슬롯 전이, 브로커 cancel_order/poll
- S-2 측정 방식: 시장가/지정가 + slippage 산출 단언 + 미지정 회귀 / 데이터 출처: OrderRequest limit_price, 슬롯
- S-3 측정 방식: 취소-체결 동시 시나리오 수렴 + 운영자 cancel 백스톱 / 데이터 출처: poll_fill/cncl_yn, control RPC
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: TIF 범위(OQ-1 timeout만)·슬리피지 규칙(OQ-2 entry_price 밴드) 합의 → In/Out 경계 명확
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~3 측정 시나리오·데이터 출처가 테스트 가능한 형태로 정의됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(취소-체결 race)은 FR-4(취소 송신·확인 분리)로 해소 방향 확정. 해소 책임은 구현 batch.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~4 전부 Answered (2026-06-10 사용자 결정)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자 Justin Ko confirm-discovery 수행
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 갱신 (주축 재작성)
- 생성 경로: docs/discovery/dcy-022_20260610_stuck-pending-order-recovery.md
- 참조 문서:
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md (자산 중심 영구 슬롯·slot_id 정본·상태머신 — 토대)
- docs/discovery/dcy-021_20260610_position-sizing-target-intent.md (부분 매도 — 실행 정책 적용 대상)
- docs/discovery/dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md (stale Alert-only + cncl_yn 롤백·부분 취소 회계 선례)
- docs/interface-contract.md(buy/sell payload), docs/runtime-flows.md (baseline — 갱신 대상)
- GitHub issue #21(pending 고착)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- 없음 (OQ-1~4 전부 Answered — §6 참조)
## CONFIRM
- 없음 (전제 dcy-020/021 구현은 BAT-027~031 로 충족 — A-1)
## DATA
- 없음 (외부 데이터 의존 없음 — 내부 도메인/브로커 계약 + 전략 발신 합의)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~4 전부 Answered, C형 플레이스홀더 0, 저장소 사실 재확인 일치(cancel_order 어댑터 존재·미배선, payload 무 timeout/slippage, stale Alert-only). REQ Drafting 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-10 19:30 KST
@@ -1,208 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-023_20260610_allocated-capital-dead-field-removal
- 이슈명: allocated_capital 죽은 필드 제거 — `enter --capital` 옵션 + slot/intent/snapshot 계약 정리 (issue #22)
- 작성 시각(KST): 2026-06-10 21:27
- 마지막 갱신 시각(KST): 2026-06-10 21:42
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 신규 REQ 없음 — change-req 5건: REQ-002(CHG-20260610-02)·REQ-006(CHG-20260610-02)·REQ-007(CHG-20260610-02)·REQ-028(CHG-20260610-01)·REQ-032(CHG-20260610-07)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #22 (`trex-core enter --capital 옵션 및 slot allocated_capital 필드 제거`)
- 해석:
- issue #22 가 지적한 대로 `allocated_capital` 은 주문 사이징·회계·전략 결정 어디에도 쓰이지 않는 죽은 메타데이터다 — 설정(CLI)·운반(intent)·영속(daily_slot)·표시(snapshot) 경로만 존재한다.
- dcy-021(포지션 사이징)에서 "전략이 cash+quantity 로 qty 를 계산해 명령형 intent 로 제출, core 는 사이징하지 않음" 계약이 확정되어 capital 기반 자동 사이징의 미래 용도도 사라졌으므로, 필드 제거의 전제(OQ-3)가 사실상 해소된 상태에서 제거 범위·마이그레이션 정책만 결정하면 된다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery 22건 중 `allocated_capital` 제거를 다루는 문서 없음. 가장 가까운 dcy-021(포지션 사이징)은 qty 사이징 계약 확립이 주제이고, 본 건은 그 결정의 귀결인 죽은 필드 정리로 문제 정의가 다르다.
- 슬롯 데이터 계약·영속(컬럼 drop)·intent payload·CLI·snapshot 노출에 걸치는 다중 REQ change-req 묶음이라 독립 주기로 추적하는 편이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `allocated_capital` 소비처는 전수 추적 결과 설정·운반·영속·표시뿐이다: `trex_core/runtime/cli.py`(`--capital`→payload, slot detail 표시), `trex_core/application/intents.py`(BuyIntent 필드 + admission 전달), `trex_core/application/messaging_handlers.py`(payload 파싱), `trex_core/domain/slot/models.py`(Slot 필드 + `EXPECTED_SLOT_FIELDS`), `trex_core/domain/slot/service.py`(buy/전이 시 설정·클리어), `trex_core/adapters/persistence/postgresql.py`(daily_slot 컬럼 SELECT/UPDATE/복원), `trex_core/application/portfolio.py`(snapshot `slots[]` 노출). 의사결정/회계/사이징 소비자는 없다.
- 집행 수량은 `qty = int(slot.qty or 0)` 로 `slot.qty` 에서만 온다. issue #22 가 지적한 footgun(`--capital` 만 주고 `--qty` 누락 → 0주 주문)의 CLI 측면은 BAT-033(REQ-032 AC-7, `--qty` 필수 검증)으로 이미 차단됐으나, 필드 자체는 슬롯 계약·영속·스냅샷에 잔존해 유지 비용과 "capital 이 사이징한다"는 오해 여지가 남아 있다.
- 마이그레이션은 012 까지 존재하며, BAT-030 의 011(`drop_pool_model_columns`)이 비가역 컬럼 drop + 사전 백업 운영 절차의 선례를 남겼다.
- baseline 문서 `docs/project-structure.md`, `docs/runtime-flows.md` 존재 — 확인 결과 두 문서 모두 `allocated_capital` 을 직접 기술하지 않으므로 FR-5(baseline 정합)는 제거 후 재확인 수준의 경량 항목이다.
- 기대 상태
- `trex-core enter` 에서 `--capital` 옵션이 제거되고, buy intent payload·Slot 모델·daily_slot 컬럼·snapshot `slots[]` 에서 `allocated_capital` 이 사라진다.
- 슬롯 데이터 계약이 실제 소비되는 필드만 남아 계약·영속·스냅샷·테스트의 유지 비용이 줄고, capital 기반 사이징 오해가 원천 제거된다.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/batch 산출물 작성
- 운영자(Justin Ko) — CLI 인터페이스 변경(`--capital` 제거)과 마이그레이션(컬럼 drop) 승인
- 전략 모듈 발신자 — buy intent payload 계약 변경의 영향 대상(현재 `allocated_capital` 송신 여부 확인 필요)
- 제약사항
- daily_slot 컬럼 drop 은 비가역 — BAT-030 선례처럼 사전 백업 + 롤백 절차 명시 필수.
- 운영 라이브 DB 에 대한 파괴적 작업 금지 원칙 — 마이그레이션 검증은 테스트 DSN(trex_core_test)에서 수행.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 `trex-core enter <pf> --qty 10 --yes` 로 진입한다 — `--capital` 은 더 이상 받지 않으며, 잘못 입력하면 argparse 가 즉시 거부해 "capital 로 사이징된다"는 오해가 생길 여지가 없다.
- 전략 모듈이 buy intent 를 제출할 때 payload 에 `allocated_capital` 을 넣지 않는다(또는 deprecated 기간 동안 넣어도 무시된다) — 계약 문서와 실제 동작이 일치한다.
- 개발자가 Slot 모델/persistence/snapshot 코드를 읽을 때 죽은 필드가 없어 실제 소비되는 계약만 추적하면 된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- `trex-core enter` 의 `--capital` 옵션 제거 + buy intent payload 의 `allocated_capital` 필드 폐기(즉시 제거 vs deprecated 수신-무시 중 OQ-1 결정).
- Slot 도메인 모델·`EXPECTED_SLOT_FIELDS`·persistence flat 매핑·daily_slot 컬럼(마이그레이션 013, drop)·snapshot `slots[]` 노출에서 `allocated_capital` 제거.
- 데이터 계약 변경에 따른 baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`) 슬롯 필드 기술 정합 갱신(해당 기술이 있는 경우).
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/runtime/cli.py`(`--capital` 인자·summary·payload·slot detail 표시), `trex_core/application/intents.py`(BuyIntent 필드·admission), `trex_core/application/messaging_handlers.py`(payload 파싱), `trex_core/domain/slot/models.py`(+`EXPECTED_SLOT_FIELDS`), `trex_core/domain/slot/service.py`(설정/클리어 경로), `trex_core/adapters/persistence/postgresql.py`(컬럼 매핑), `trex_core/application/portfolio.py`(snapshot 노출)
- 신규 마이그레이션 `013_drop_allocated_capital.sql`(ALTER TABLE daily_slot DROP COLUMN IF EXISTS, 멱등) + 관련 단위 테스트 정리
- 문서 영향:
- REQ-002(슬롯 도메인)·REQ-006(persistence/마이그레이션)·REQ-007(buy intent payload)·REQ-028(snapshot slots[])·REQ-032(운영자 CLI) change-req
- baseline `docs/project-structure.md`/`docs/runtime-flows.md` 슬롯 필드 기술 정합(존재 시)
- 운영 영향:
- 마이그레이션 013 적용(비가역 drop — 사전 백업 절차), `enter --capital` 사용 스크립트/습관 변경 공지. 데몬 재기동 1회.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `trex-core enter` 는 `--capital` 옵션을 받지 않는다. 지정 시 argparse 수준에서 unknown argument 로 거부된다.
- FR-2: buy intent payload 계약에서 `allocated_capital` 이 즉시 제거된다 — messaging handler 의 파싱 제거로 외부 발신자가 키를 계속 보내도 unknown key 로 무시된다(OQ-1 결정: 즉시 제거).
- FR-3: Slot 모델·`EXPECTED_SLOT_FIELDS`·persistence 매핑·daily_slot 컬럼에서 `allocated_capital` 이 제거된다(마이그레이션 013, `DROP COLUMN IF EXISTS` 멱등).
- FR-4: snapshot `slots[]` 응답과 CLI `slots` detail 표시에서 `allocated_capital` 필드가 사라진다.
- FR-5: baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)에 슬롯 필드/intent 계약 기술이 있으면 제거 결과와 정합하도록 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 마이그레이션 013 은 멱등(`IF EXISTS`)이며, 구 코드와의 공존이 불가능한 비가역 drop 이므로 적용 전 백업·롤백 절차를 release 문서에 명시한다(BAT-030 011 선례 준용).
- NFR-2: 전체 단위 테스트 suite 회귀 없음(제거 대상 필드를 참조하던 테스트는 함께 정리).
- NFR-3: 변경 후에도 buy intent admission·집행 경로의 런타임 동작(qty 기반 사이징)은 불변이다 — 죽은 필드 제거가 거래 동작을 바꾸지 않는다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- CLI `--capital` 제거, buy intent `allocated_capital` 폐기, Slot/persistence/snapshot 필드 제거, 마이그레이션 013, 관련 REQ change-req 5건, baseline 정합
- Out of Scope
- capital 기반 자동 사이징(capital→qty) 도입 — dcy-021 에서 "전략이 사이징, core 미관여" 로 확정되어 채택하지 않음
- `entry_price`/`limit_price` 등 다른 슬롯 필드 정리 — 본 건과 무관
- trex-orb-v1 등 외부 전략 모듈 저장소의 발신 코드 수정 — 사용자 확인 결과 무시(범위 제외). RabbitMQ payload 는 관용적 계약이라 송신 잔존이 무해함.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Low): 외부 발신자(trex-orb-v1, `intent_builder.py:56`)가 `allocated_capital` 을 계속 송신함 — 파싱 제거 후에는 unknown key 로 무시되어 동작 영향 없음(사용자 확인: 외부 발신자 무시).
- 완화 방안: messaging handler 파싱 제거만으로 충분. 외부 저장소 수정은 범위 제외.
- R-2 (Medium): daily_slot 컬럼 drop 은 비가역 — 롤백 시 구 코드는 해당 컬럼을 UPDATE 절에 포함하므로 컬럼 부재 시 구 리비전과 공존 불가.
- 완화 방안: BAT-030 011 선례 준용 — 적용 전 daily_slot 백업, release 문서에 롤백 절차(컬럼 재추가 + 백업 복원) 명시, 테스트 DSN 에서 round-trip 검증 후 적용.
- R-3 (Low): 제거 대상 필드를 참조하는 테스트·스냅샷 fixture 누락으로 suite 가 깨질 수 있음.
- 완화 방안: `allocated_capital` 전수 grep(소스+테스트) 후 일괄 정리, 전체 suite green 을 DoD 로 설정.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: capital 기반 자동 사이징은 향후에도 core 책임이 아니다 — dcy-021 확정 계약(전략이 cash+quantity 로 qty 계산, 명령형 intent)이 유지된다.
- A-2: 운영 daily_slot 의 기존 `allocated_capital` 값은 보존 가치가 없다(의사결정·회계 소비자가 없었으므로 이력 데이터로도 무의미). 필요 시 drop 전 백업으로 충분하다.
- A-3: BAT-033 의 `--qty` 필수 검증이 release 되면 `--capital` 단독 입력 footgun 은 이미 차단된 상태에서 본 건은 순수 계약 정리다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: buy intent payload 의 `allocated_capital` 을 즉시 제거(수신 시 무시)할지, deprecated 명시 후 유예 기간을 둘지.
- 상태: Answered — **즉시 제거** (2026-06-10, 운영자 결정)
- 근거: RabbitMQ payload 는 관용적 계약이라 파싱 제거 = 수신-무시이며 유예가 주는 추가 안전이 없음. trex-orb-v1 이 실제 송신 중이나(intent_builder.py:56) 사용자 확인으로 무시(범위 제외). FR-2 에 반영 완료.
- 종료 조건: 충족 (FR-2 본문 반영).
- OQ-2: 마이그레이션 013 의 적용 시점 — 코드 배포와 동시 적용 vs 코드 먼저(컬럼 미사용화) 배포 후 별도 시점 drop.
- 상태: Answered — **동시 적용** (2026-06-10, 운영자 결정)
- 근거: 단일 인스턴스 운영 + BAT-030 마이그레이션 011(pool-model 컬럼 drop) 동시 적용 선례. 롤백은 "백업 복원 + 구 리비전"으로 동일하게 커버. `apply_migrations` 가 데몬 기동 시 자동 적용하므로 별도 절차 불필요.
- 종료 조건: 충족 (REQ-006 change-req 에 동시 적용 + 사전 백업 절차로 반영 예정).
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 소스 트리(`trex_core/`)에서 `allocated_capital` 참조가 0건이 된다(deprecated 유예를 택한 경우 messaging handler 의 무시 주석 1건만 예외).
- S-2: 마이그레이션 013 적용 후 daily_slot 에 `allocated_capital` 컬럼이 없고, `EXPECTED_SLOT_FIELDS` ↔ information_schema 계약 가드가 green 이다.
- S-3: 전체 단위 테스트 suite green(회귀 0) + `trex-core enter --capital ...` 이 argparse 오류로 거부된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: `grep -rn allocated_capital trex_core/ tests/` 결과 0건(또는 예외 1건) / 데이터 출처: 저장소 소스 트리
- S-2 측정 방식: 테스트 DSN 에서 013 적용 후 information_schema 조회 + 컬럼 계약 가드 테스트 / 데이터 출처: trex_core_test PostgreSQL
- S-3 측정 방식: `pytest -q` 전체 green + CLI 수동 스모크(`enter --capital` 거부 확인) / 데이터 출처: 테스트 실행 로그
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 외부 전략 모듈 저장소 수정 포함 여부 확인 완료 — 운영자 확인으로 범위 제외 확정 (2026-06-10)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3 측정 방식에 대한 사용자 이견 없음 — grep 전수·계약 가드 테스트·suite green 모두 저장소 근거로 측정 가능
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-2(비가역 drop) 완화 절차(백업·롤백, BAT-030 011 선례 준용)를 운영자가 OQ-2 동시 적용 결정과 함께 승인 (2026-06-10)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1(즉시 제거)·OQ-2(동시 적용) 모두 Answered (2026-06-10)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 지정 (2026-06-10, 사용자 명시)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-023_20260610_allocated-capital-dead-field-removal.md
- 참조 문서:
- GitHub issue #22 (trex-core enter --capital 옵션 및 slot allocated_capital 필드 제거)
- docs/discovery/dcy-021_20260610_position-sizing-target-intent.md (사이징 계약 확정 — capital 사이징 불채택 근거)
- trex_core/adapters/persistence/sql/011_drop_pool_model_columns.sql (비가역 컬럼 drop 선례)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~OQ-1 — buy intent `allocated_capital` 즉시 제거 vs deprecated 유예~~ → **즉시 제거 확정** (2026-06-10, 운영자)
- ~~OQ-2 — 마이그레이션 013 동시 적용 vs 2단계 분리~~ → **동시 적용 확정** (2026-06-10, 운영자)
## CONFIRM
- ~~외부 전략 모듈(trex-orb-v1 등)의 송신 코드 정리 범위 포함 여부~~ → **무시(범위 제외) 확정** (2026-06-10, 운영자)
## DATA
- ~~trex-orb-v1 의 `allocated_capital` 송신 여부~~ → **송신함** (`trex_orb_v1/runtime/intent_builder.py:56`). 단 사이징 입력은 자체 계산(`daemon.py:83`)이며 trex-core snapshot 의존 아님. 운영자 결정으로 무시.
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1(즉시 제거)·OQ-2(동시 적용) 모두 운영자 결정으로 Answered, 외부 발신자 확인 완료(무시·범위 제외). C 유형 플레이스홀더 0건, baseline 문서 모순 없음(allocated_capital 직접 기술 없음 확인). REQ change-req 5건(REQ-002·006·007·028·032) 인계 준비 완료.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-10 21:42
@@ -1,194 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-024_20260611_safety-sell-disable-mode
- 이슈명: safety-sell 자율 청산 비활성화 모드 (issue #15)
- 작성 시각(KST): 2026-06-11
- 마지막 갱신 시각(KST): 2026-06-11 13:50 KST (change-req 진행: REQ-048 CHG-20260611-01(safety_sell_enabled TOML) + REQ-005 CHG-20260611-01(조건부 워커·기동 로그·runtime-flows), 둘 다 Implemented→Approved. 신규 REQ 없음(OQ-1).)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 없음 (change-req only, dcy-024 OQ-1). Delivery: REQ-048(CHG-20260611-01)·REQ-005(CHG-20260611-01) change-req → 차기 batch.
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 점수 파일: 없음
- 관련 TODO/메모: GitHub issue #15 (자율 청산 비활성화 모드 설계 전문 포함)
- 해석:
- REL-006 피드백 환류 Discovery 후보로 식별됨. 자율 청산(forced_close / SL·TP / market_close) 워커를 완전히 끄고 전략·운영자 지시 매도만 허용하는 운영 모드 요구.
- REQ-048(per-instance TOML) + REQ-005(daemon bootstrap) change-req 2건으로 닫는다 — OQ-1·OQ-2 확정. SafetySellConfig(env) 변경 없음.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-007(core-autonomous-safety-sell)은 자율 매도 기능을 구현한 Discovery로 REL-003에서 released됨. 비활성화 모드는 그 기능의 확장이 아닌 운영 정책 플래그 추가 — 독립 후속 주기.
- 현재 SafetySellConfig에 `enabled` 플래그가 없고, `daemon.start()`의 workers 리스트에 `trex-safety-sell` 스레드가 조건 없이 포함됨. 코드 갭이 실존하는 신규 요구.
# 3. 문제점의 요약
- 현재 상태
- `trex_core/runtime/daemon.py:335`: `("trex-safety-sell", self._safety_sell_loop)` 이 workers 리스트에 항상 포함되어 데몬 기동 시 매번 자율 청산 워커가 시작됨.
- `SafetySellConfig`(config.py:113)에는 `enabled` 플래그가 없고, per-trigger 비활성화 수단도 없음. 관찰 전용 인스턴스·신규 전략 검증 환경에서 자율 청산을 막을 방법이 없음.
- 기대 상태
- `DaemonInstanceConfig`(per-instance TOML)에 `safety_sell_enabled: bool = True` 추가(OQ-2 — env 제외, `max_active_slots`/`default_order_timeout_sec` 선례)로, `false` 시 `trex-safety-sell` 스레드를 데몬 기동부에서 아예 시작하지 않음.
- 기동 로그에 `safety_sell=disabled` 표시로 운영자가 안전망 없는 인스턴스임을 즉시 인지 가능(스냅샷 응답 미포함 — OQ-3).
- 이해관계자
- 운영자/시스템 관리자 (Justin Ko): 관찰 전용 인스턴스·신규 전략 검증 시 자율 청산 OFF 필요
- 전략 모듈(trex-orb-v1): 전략 지시 매도는 항상 유지되어야 함
- 리뷰어/승인자: 안전망 없음을 명시한 운영 가이드 확인
- 제약사항
- `ExecutionService`(집행 루프), 운영자 CLI, kill-switch는 이 플래그에 영향받지 않아야 함 — 자율 청산(safety-sell 워커)만 OFF.
- Redis·RabbitMQ·PostgreSQL 의존성 변경 없음. config.py + daemon.py 한정 수정.
## 주요 사용자 시나리오
- 관찰 전용 인스턴스: per-instance TOML `safety_sell_enabled = false` 설정 후 데몬 기동 → 기동 로그에 `safety_sell=disabled` 확인 → 포지션 유지 중 자동 청산 미발생, 전략 sell intent는 정상 집행.
- 신규 전략 검증(paper 브로커): safety-sell 없이 전략 모듈 매도 타이밍만 테스트 — 의도치 않은 장마감 강제청산 없이 전략 e2e 검증.
- 기본 운영(enabled=true): 기존 동작과 완전 동일. 하위 호환 변경 없음.
# 4. 이번에 정의할 변경
- `DaemonInstanceConfig`(per-instance TOML)에 `safety_sell_enabled: bool = True` 필드 추가(OQ-2 — env 제외, SafetySellConfig 변경 없음).
- `daemon.start()` workers 리스트 구성부에서 `safety_sell_enabled=false` 시 `trex-safety-sell` 스레드 제외 — run_once 내부 if-가드보다 스레드 미기동이 물리적으로 자율 청산을 차단.
- 기동 로그(`RUNNING`)에 `safety_sell_enabled` 상태 노출(스냅샷 응답 미포함 — OQ-3) — 운영자가 인지 없이 안전망 없는 인스턴스를 운영하는 사고 방지.
## 영향 범위
- 코드 영향:
- `trex_core/runtime/config.py`: `DaemonInstanceConfig`에 `safety_sell_enabled: bool = True` 필드 추가 + `from_toml()` 파싱 (SafetySellConfig 변경 없음)
- `trex_core/runtime/daemon.py`: `start()` workers 조건부 구성 — `config.instance.safety_sell_enabled` 참조 + 기동 로그 `safety_sell=disabled/enabled`
- 문서 영향:
- REQ-048(per-instance TOML 설정) change-req: `safety_sell_enabled` 필드 추가
- REQ-005(daemon bootstrap) change-req: 워커 조건부 기동 명시
- `docs/runtime-flows.md`: safety-sell 비활성 모드 플로우 추가
- 운영 영향:
- 기본값 `true` — 기존 운영 인스턴스 동작 무변. 기동 전 `.env` 또는 TOML에 `TREX_SAFETY_SELL_ENABLED=false` 추가 시에만 비활성 모드로 전환.
- 비활성 모드 인스턴스는 손절/강제만기/장마감 안전망 없음 — 운영 가이드·기동 로그 명시 필수.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: `DaemonInstanceConfig`(per-instance TOML)에 `safety_sell_enabled: bool = True` 필드를 추가하고, TOML `safety_sell_enabled = false`로 인스턴스별 비활성화를 제어한다. 기본값 `true`(미지정 시 기존 동작 유지). env var 경로 미노출 — 인스턴스별 운영 모드 차이(`max_active_slots` 선례).
- FR-2: `daemon.start()` 시 `DaemonInstanceConfig.safety_sell_enabled == False` 이면 `trex-safety-sell` 워커 스레드를 workers 리스트(`daemon.py:335`)에 포함하지 않는다. `True`(기본)이면 기존과 동일하게 기동한다.
- FR-3: 기동 시 RUNNING 로그에 `safety_sell_enabled=<true|false>` 필드를 포함한다. 비활성 모드(`false`)이면 추가로 `no autonomous liquidation` 경고를 로그에 남긴다.
- FR-4: `safety_sell_enabled` 모드는 **기동 로그(FR-3)로만** 노출하고 **스냅샷/slots 응답에는 포함하지 않는다**(OQ-3). 근거: 인스턴스 config 이지 per-slot 데이터가 아니며, dcy-025 가 read 표면을 "응답=상태(slots[])+health"로 최소화하므로 config 를 응답에 싣지 않는 패턴과 정합한다. 런타임 노출이 필요해지면 per-slot 이 아니라 dcy-025 envelope-level(또는 별도 instance-status 표면)에서 조율한다.
## 비기능 요구사항
- NFR-1: `enabled=false` 시 `ExecutionService`(집행 루프)·운영자 CLI(`enter`/`exit`/`cancel`)·kill-switch·messaging handler에는 영향 없음. 자율 청산(safety-sell 워커)만 미기동.
- NFR-2: 기본값 `true` — 기존 운영 인스턴스 재배포 시 config 변경 없이 기존 동작 보존(완전 하위 호환).
- NFR-3: 비활성 모드는 테스트에서 `config.safety_sell.enabled=False`를 주입하는 것만으로 재현 가능. 추가 mocking 불필요.
## 범위 경계
- In Scope
- SafetySellConfig `enabled` 필드 + env + TOML 파싱
- daemon.start() workers 조건부 구성
- 기동 로그에 모드 노출(스냅샷 응답 미포함 — OQ-3)
- REQ-048(per-instance TOML) change-req + REQ-005(daemon bootstrap) change-req + runtime-flows 갱신
- Out of Scope
- per-trigger 개별 플래그(forced_close / SL·TP / market_close 각각 ON/OFF) — 마스터 플래그 이후 별도 후속 이슈로 분리
- kill-switch 포함 여부 변경 — 운영자 지시 경로이므로 유지(혼용 금지)
- 비활성 모드용 별도 watchdog 가시화 — 워커 미기동 시 로깅 없어도 무방(과설계 회피)
- trex-orb-v1 전략 모듈 변경 — 전략 sell intent는 messaging handler 경로이며 영향 없음
# 6. 리스크/가정 목록
## 리스크
- R-1 (Medium): 비활성 모드 인스턴스에서 전략 모듈이 죽어 폴링이 끊겨도 안전망 없이 포지션이 계속 유지됨 — 의도적 모드이지만 운영자 인지 없이 설정하면 무제한 손실 리스크.
- 완화 방안: 기동 로그 `no autonomous liquidation` 경고로 운영자가 즉시 인지(스냅샷 응답 미포함 — OQ-3). 운영 가이드에 위험 명시.
- R-2 (Low): TOML `safety_sell_enabled` 오타/잘못된 타입이 의도와 다르게 해석될 경우 기대와 반대로 자율 청산이 활성화됨.
- 완화 방안: TOML bool 파싱 규약을 따르되(미지정/비-false → `true`), 기동 로그에 파싱된 값을 항상 출력해 운영자가 확인 가능하게 함.
- R-3 (Low): 기존 SafetySellConfig 단위 테스트가 `enabled` 필드 추가 후 drift할 수 있음.
- 완화 방안: `EXPECTED_SLOT_FIELDS` drift test 선례처럼 config 계약 가드 테스트 추가 또는 기존 테스트 보완.
## 가정
- A-1: 단일 마스터 플래그(`enabled`) 구현으로 시작하고, per-trigger 세분화는 후속 이슈로 분리한다.
- A-2: `enabled=false` 시 kill-switch, 운영자 CLI exit, 전략 sell intent는 그대로 동작한다.
- A-3: 신규 REQ 없이 REQ-048(per-instance TOML) + REQ-005(daemon bootstrap) change-req 2건으로 닫는다 — OQ-1·OQ-2 Justin Ko 확정. SafetySellConfig(env) 변경 없음.
## 오픈 질문
- OQ-1: 신규 REQ 없이 change-req 경로(REQ-041·005)만으로 진행할지, 별도 Configuration REQ를 신규 발행할지?
- 상태: Answered
- 처리 방안: Justin Ko 확정 — **change-req 우선(신규 REQ 미발행)**. REQ-048(DaemonInstanceConfig TOML) + REQ-005(daemon bootstrap) change-req 2건으로 닫는다.
- 종료 조건: 완료
- OQ-2: per-instance TOML `[safety_sell] enabled = false` 지원 포함 여부(env만 지원할지 TOML도 포함할지)?
- 상태: Answered
- 처리 방안: Justin Ko 확정 — **TOML 포함(env 제외)**. 당일 매매 인스턴스는 enabled=true, 중장기 보유 인스턴스는 enabled=false — 인스턴스마다 다른 운영 모드이므로 `DaemonInstanceConfig`(TOML)에 배치. `max_active_slots`·`default_order_timeout_sec` 선례 동일. REQ-048 change-req 추가.
- 종료 조건: 완료
- OQ-3: 스냅샷 응답(`slots[]` envelope)에 `safety_sell_enabled` 필드 추가 포함 여부(FR-4)?
- 상태: Answered (2026-06-11, 사용자 결정)
- 결정: **기동 로그만, 스냅샷/slots 응답 미포함.** 근거: `safety_sell_enabled`는 인스턴스 config(per-slot 데이터 아님)이고, dcy-025 가 read 표면을 "응답=상태(slots[])+health"로 최소화하므로 config 를 응답에 싣지 않는 패턴과 정합. **per-slot 행에 넣지 않는다.** 런타임 노출이 필요해지면 per-slot 이 아니라 dcy-025 envelope-level(또는 별도 instance-status 표면)에서 조율(후속). 반영: FR-4.
# 7. 초기 성공 기준
- S-1: per-instance TOML `safety_sell_enabled = false`로 데몬 기동 시 `trex-safety-sell` 스레드가 시작되지 않는다. 기동 로그에 `safety_sell_enabled=false`와 `no autonomous liquidation` 경고가 포함된다.
- S-2: 비활성 모드에서 forced_close_at 만기·SL·TP·장마감 시각이 지나도 슬롯이 자동 청산되지 않는다. 전략 모듈 sell intent + 운영자 CLI exit는 정상 집행된다.
- S-3: `enabled=true`(기본값) 시 기존 동작과 동일하며 기존 단위·통합 테스트 회귀가 없다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: 단위 테스트 — `enabled=False` config 주입 + `daemon.start()` 후 thread 이름 목록에 `trex-safety-sell` 부재 확인 / 데이터 출처: `tests/runtime/test_daemon.py` 또는 신규 테스트
- S-2 측정 방식: run-the-app 스모크 — 비활성 모드 데몬 기동 후 holding 슬롯 유지 중 장마감 시각 경과 → 슬롯 상태 `holding` 유지 + 전략 sell intent 처리 확인 / 데이터 출처: 데몬 기동 로그(`safety_sell_enabled=false`) + 슬롯 상태(`trex-core slots`로 holding 유지 확인 — slots 출력 자체엔 safety_sell_enabled 미포함, OQ-3)
- S-3 측정 방식: `pytest` 전체 실행 회귀 없음, `ruff check` clean / 데이터 출처: CI 또는 로컬 테스트 결과
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: per-trigger 플래그 Out of Scope + TOML 지원(OQ-2 TOML·env 제외) 확정
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1 단위(스레드 부재)·S-2 run-the-app 스모크·S-3 회귀로 정의됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(비활성 모드 손실)은 기동 로그 경고 + 운영 가이드로 해소, 가이드 작성 담당 = 구현 batch/Justin Ko(아래)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1/2/3 전부 Answered
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 승인 후 `confirm-discovery` 수행
- [ ] (delivery 인계) 비활성 모드 운영 가이드(손실 리스크 경고) 작성 — 구현 batch 단계 산출물, 담당 project maintainers/Justin Ko (Discovery confirm 비차단)
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-024_20260611_safety-sell-disable-mode.md
- 참조 문서:
- GitHub issue #15 — 자율 청산 비활성화 모드 설계 전문 (영향 범위·구현 방안·OQ 상세)
- docs/discovery/dcy-007_20260606_core-autonomous-safety-sell.md — 자율 매도 구현 Discovery (선행 참조)
- trex_core/runtime/config.py:113 — SafetySellConfig (변경 대상)
- trex_core/runtime/daemon.py:335 — workers 리스트 (`trex-safety-sell` 항목, 변경 대상)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- 없음 (OQ-1·OQ-2 확정 완료)
## CONFIRM
- 없음. (1) 스냅샷 응답 노출 = OQ-3 Answered(기동 로그만). (2) kill-switch·운영자 CLI exit가 `safety_sell_enabled=false`와 무관하게 유지됨은 설계로 확정(A-2/NFR-1) — 저장소상 `KillSwitchService`(daemon.py:166)·운영자 CLI exit 는 `trex-safety-sell` 워커와 **독립 경로**라 워커 미기동이 그 둘에 영향 없음(확인 완료).
## DATA
- 없음. (운영 가이드(손실 리스크 경고) 작성은 Discovery 정책 결정이 아니라 **delivery 단계 산출물**이므로 §8 인계 체크로 이동 — 담당은 구현 batch/project maintainers.)
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1/2/3 전부 Answered(change-req 경로·TOML·기동 로그만). C형 플레이스홀더 0(CONFIRM/DATA 해소). 저장소 사실 재확인 일치. 운영 가이드 작성만 delivery 단계 비차단 인계. REQ Drafting 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-11 13:29 KST
@@ -1,228 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-025_20260611_strategy-read-surface-slots-req
- 이슈명: 전략 read 표면 재설계 — `trex.snapshot.req`→`trex.slots.req` + 슬롯 상태 필터 + 슬롯 단일 행(assets[]/available_slots/watchdog[] 흡수·제거)
- 작성 시각(KST): 2026-06-11 12:38 KST
- 마지막 갱신 시각(KST): 2026-06-11 14:05 KST (draft-req 완료: REQ-073/074/075 신규 + REQ-021/034/063 change-req(Approved 강등) + REQ-028 폐기 예정 forward-link)
- 대체됨: 없음
- 후속 Discovery 참조: 전략↔자산 할당 모델(portfolio-hub 주도 다중 전략 설정) — 미생성, 후속 예정(본 표면에 strategy 필드 additive 추가 시점)
- 생성된 REQ 참조: REQ-073(slots.req RPC 계약), REQ-074(슬롯 단일 행 스키마), REQ-075(baseline 정합). 변경 REQ: REQ-021/034/063(각 CHG-20260611-01, Implemented→Approved), REQ-028(CHG-20260611-01, keep-current — delivery 배치에서 Deprecated 전환)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: dcy-020(자산 중심 영구 슬롯·REQ-063 slots 단일 뷰 — 직접 토대), BAT-022(universe↔slots 이중 표면 분리 선례), dcy-005(스냅샷 universe read 기원), REQ-028(snapshot 메시지 계약), REQ-021(messaging 토폴로지).
- 해석:
- dcy-020 이 "슬롯=자산 1:1 영구 슬롯"으로 모델을 바꾸면서, 전략이 universe 를 받던 스냅샷 표면(`trex.snapshot.req`, view=all/buyable/held + `assets[]`/`slots[]`/`available_slots`/`watchdog[]`)이 과도하게 분리·중복된 상태가 됐다. 자산 중심 모델에선 **슬롯 테이블 하나**가 곧 universe 이므로 `assets[]`·`watchdog[]` 같은 병렬 배열을 슬롯 행에 흡수할 수 있다.
- 본 Discovery 는 전략 read 표면을 **`trex.slots.req`(슬롯 단일 테이블 + 상태 필터)**로 재설계한다 — `assets[]`/`available_slots`/`watchdog[]` 제거, watchdog·자산 데이터(symbol/portfolio_id/cash_balance/asset_quantity)를 슬롯 행에 흡수, `spy`(워치독 무터치) 옵션, `degraded/reason/message` 유지. BAT-022·dcy-020(REQ-063)가 향하던 통합의 종착점이다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 큐 표면 교체(`trex.snapshot.req`→`trex.slots.req`)·응답 구조 재설계(assets/available_slots/watchdog 제거 + 슬롯 행 흡수)는 REQ-028(snapshot 계약)·REQ-021(토폴로지)·REQ-063(slots 뷰)을 동시에 재정의하는 인터페이스 변경이라 단일 REQ 미세 조정으로 흡수되지 않는다.
- dcy-020 이 자산 중심 모델을 confirmed·delivery(BAT-027~031)했으나 전략 read 표면의 통합은 별도 주제로 남겨졌다. 본 주기는 그 read 계약을 독립적으로 재설계한다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **요청 표면 `trex.snapshot.req`**: payload `{view: all|buyable|held, strategy_key?, trading_date?, actor?/touch_watchdog?}`(`messaging_handlers.py` snapshot 핸들러). view 는 파생 집합(`View` enum, `portfolio.py:65`): ALL=universe∪holding, BUYABLE=universe−occupied−masked, HELD=holding.
- **응답 envelope**(`build_snapshot_envelope`): `assets[]`(view 필터된 자산: symbol/asset_id/cash_balance/asset_quantity/active_strategy_id/strategy_params) + `slots[]`(인스턴스 전체 슬롯 테이블, view 무관 — BAT-022) + `available_slots`(M−active capacity) + `degraded`/`reason`/`message` + `watchdog[]`(별도 배열, `_attach_watchdog`).
- **이중 표면·병렬 배열**: `assets[]`(universe view)와 `slots[]`(슬롯 테이블)가 공존하고, `watchdog[]`는 슬롯과 조인해야 하는 별도 배열이다. 자산 중심 모델(슬롯=자산 1:1)에선 `assets[]`가 사실상 `slots[]`의 부분집합이라 중복이다.
- **워치독 터치 시맨틱**: 전략 폴링은 holding 슬롯 워치독을 갱신(생존 신호, REQ-034)하고, 운영자 조회는 `actor="operator"`/`touch_watchdog=false`로 억제(REQ-021 R-4) — 두 갈래가 ad-hoc 플래그로 표현됨.
- **strategy 필터**: `strategy_key`로 소유 자산 필터, `assets[]`에 `active_strategy_id`/`strategy_params` 노출.
- legacy stub 경로(`{view, strategy_id, slots[], capacity{}}`)도 잔존(구버전·테스트 호환).
- baseline(`docs/interface-contract.md` snapshot 섹션)이 이 표면을 기술 — 재설계 시 갱신 대상.
- 기대 상태
- 전략이 **`trex.slots.req`** 하나로 슬롯 테이블을 받는다. 응답 = `slots[]` + `degraded`/`reason`/`message` 만.
- `slots[]` 각 행이 슬롯 상태(slot_id/status/qty/avg_price/limit_price/forced_close_at…)와 자산 데이터(symbol/portfolio_id/cash_balance/asset_quantity)와 **워치독 상태(expired/last_seen)**를 함께 운반한다 — 병렬 배열·조인 제거.
- `filter`로 슬롯 상태별(all/inactive/holding/pending/paused…) 조회. `spy=true`면 워치독 무터치(생존 신호로 오인 안 함).
- `assets[]`·`available_slots`·`watchdog[]` 제거. 매수 가능 목록 = `filter=inactive` 결과, capacity 는 admission(`capacity_full`)이 게이트.
- 이해관계자
- 문서 작성자(AI agent) — Discovery/REQ/Batch 산출물
- 리뷰어/승인자 — read 계약·토폴로지 재설계 승인
- 전략 모듈(orb-v1 등) — 요청 큐·응답 구조 변경(외부 repo 소비 측)
- 운영 담당자 — `spy` 조회·슬롯 단일 뷰 관측
- 제약사항
- 큐 교체는 토폴로지(REQ-021/050 동적 큐명·per-instance)·전략 모듈 소비 측을 함께 바꿔야 한다 — cutover/과도기 필요.
- 슬롯 행에 브로커 정본(qty/avg)과 hub 정본(cash_balance/asset_quantity)이 공존하며 divergence(REQ-031/062) 가능 — 필드별 source 명시 필요.
- 단일 writer·워치독(REQ-034)·degraded 신호(REQ-028)의 의미를 새 표면에서 보존.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략이 `trex.slots.req {filter: "inactive"}` 로 매수 가능(잔고 0·미보유) 슬롯만 받아 신호를 낸다 — `assets[]` 없이 `slots[]` 한 배열로.
- 전략이 `{filter: "holding"}` 로 보유 슬롯의 수량·avg·워치독 상태를 한 행에서 본다(별도 watchdog[] 조인 불필요).
- 운영자가 `{filter: "all", spy: true}` 로 전 슬롯을 조회하되 워치독을 건드리지 않아 죽은 전략의 safety-sell 을 억제하지 않는다.
- 전략이 `{filter: "pending"}` 로 진행 중 주문 슬롯을 관측한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- **요청 표면 교체**: `trex.snapshot.req` 파이프라인 제거, `trex.slots.req` 추가(토폴로지·핸들러·전략 소비 측 cutover, OQ-1).
- **슬롯 상태 필터**: `filter` = status 단일 값 `{all, inactive, pending(buy∪sell), holding, error}`(상호배타, OQ-2). `inactive` 판정=슬롯 status(잔고 아님). overlay(`paused`/`warned`)는 행 플래그(직교 축), 서버측 overlay 필터는 후속. 파생 view(ALL/BUYABLE/HELD) 폐기.
- **`spy` 옵션**: `spy=true` 면 워치독 무터치(현 `actor=operator`/`touch_watchdog=false` 일반화). 기본 false(터치=생존 신호).
- **슬롯 단일 행 흡수**(OQ-3 (a)): `slots[]` 각 행에 자산 데이터(symbol/portfolio_id/cash_balance/asset_quantity) + 워치독(`watchdog:{expired,last_seen}` nested) + overlay(`paused`/`warned`) 포함. 수량은 `asset_quantity`(hub 정본)+`qty`(미러) **둘 다** 노출(REQ-062), 어긋나면 `warned`. 현금은 `cash_balance`+`cash_estimated`. 필드별 정본 라벨 명시.
- **응답 정리**: `assets[]`·`available_slots`·`watchdog[]` **제거**, `degraded`/`reason`/`message` **유지**.
- **strategy 필드 제외(이번 범위)**: `strategy_id`/`strategy_key`/`strategy_params`/`active_strategy_id` 제외(OQ-4 — 인스턴스=전략 스코프 전제).
- baseline(`docs/interface-contract.md`/`docs/runtime-flows.md`) snapshot→slots 표면 갱신.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/adapters/messaging/topology.py` 등 — `trex.snapshot.req`→`trex.slots.req` 큐명/바인딩(per-instance, REQ-021/050)
- `trex_core/application/messaging_handlers.py` — snapshot 핸들러 → slots 핸들러(filter/spy 파싱, 응답 구조), `_attach_watchdog` 제거(슬롯 행 흡수), legacy stub 경로 정리
- `trex_core/application/portfolio.py` — `View` enum·`build_snapshot_envelope`(assets/available_slots/watchdog 제거, 슬롯 행에 자산·워치독 흡수), `slot_to_dict` 확장
- `trex_core/runtime/cli.py` — 운영자 `slots`/`universe` 명령이 새 표면 사용(`spy`)
- (전략 모듈 orb-v1 — 외부 repo 소비 측 cutover)
- 문서 영향:
- `docs/interface-contract.md`(snapshot→slots 계약), `docs/runtime-flows.md`(read 표면)
- 운영 영향:
- 전략 모듈 동시 cutover 필요(큐 교체). 운영자 조회는 `spy`로 일원화.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: `trex.snapshot.req` 를 제거하고 `trex.slots.req`(per-instance 큐명) RPC 를 추가한다 — 요청 `{filter?, spy?}`, 응답 `{slots[], degraded, reason, message}`. **즉시 교체**(과도기 없음, OQ-1): 두 큐를 한 배포에 교체하고 전략 모듈을 lockstep 전환한다.
- FR-2: `filter` 는 **status 단일 값**(상호배타) `{all, inactive, pending(=pending_buy∪pending_sell), holding, error}` 로 조회한다(OQ-2). 미지정 시 `all`. `inactive` 판정 키는 슬롯 **status**(hub `asset_quantity` 아님 — 정합 정본은 상태머신). overlay(`paused`/`warned`)는 필터 값이 아니라 각 슬롯 행의 플래그로 노출하며(FR-3), 서버측 overlay 필터는 후속(MVP는 행 플래그 + client 필터).
- FR-3: `slots[]` 각 행은 아래를 **필드별 정본 라벨**과 함께 운반한다(OQ-3 (a)):
- 슬롯/코어 정본: `slot_id`, `status`, `portfolio_id`, `symbol`, `avg_price`, `limit_price`, `forced_close_at`
- 수량(REQ-062 정합, **둘 다 노출**): `asset_quantity`(**hub 정본** — 전략 reasoning primary) + `qty`(**슬롯 미러** — 코어 confirmed local). 정상 시 일치, 어긋나면 `warned=true`. 라이브 브로커 잔고는 행에 미포함(청산/reconcile 시점 정본).
- 현금: `cash_balance`(hub 정본) + `cash_estimated`(bool, dcy-018 추정 현금 오버레이)
- 워치독: `watchdog: {expired: bool, last_seen: ISO}` (nested object)
- overlay 플래그(직교 축, OQ-2): `paused`, `warned`
- 계약 문서(interface-contract)에 각 필드 정본 출처(hub/슬롯·코어/브로커/watchdog/overlay)를 명시한다.
- FR-4: `spy=true` 면 워치독을 갱신하지 않는다(생존 신호 미발생). 기본 false(holding 슬롯 워치독 터치, REQ-034 계승).
- FR-5: 응답에서 `assets[]`·`available_slots`·`watchdog[]` 를 제거하고 `degraded`/`reason`/`message` 는 유지한다. 매수 가능 목록은 `filter=inactive`, capacity 는 admission(`capacity_full`) 게이트로 대체한다.
- FR-6: `strategy_id`/`strategy_key`/`strategy_params`/`active_strategy_id` 는 본 표면에서 제외한다(OQ-4).
- FR-7: `docs/interface-contract.md`·`docs/runtime-flows.md` 를 새 read 표면으로 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 워치독 일관성 — 전략 폴링(기본)은 생존 신호로 터치, `spy` 는 무터치(REQ-034 R-4 의미 보존).
- NFR-2: 단일 표면 — 한 응답이 슬롯 테이블 하나로 자산·워치독을 모두 담아 소비자 조인이 불필요하다.
- NFR-3: cutover 안전 — 즉시 교체(과도기 없음, OQ-1)이므로 전략 모듈(orb-v1)을 같은 배포에 lockstep 전환한다. dcy-020 자산 중심 cutover(BAT-030)와 묶어 단일 전환점으로 관리한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- `trex.slots.req` 표면(요청 filter/spy, 응답 slots[]+degraded) + 슬롯 행 흡수(자산·워치독)
- `assets[]`/`available_slots`/`watchdog[]` 제거, `trex.snapshot.req` 폐기
- interface-contract/runtime-flows 정합
- Out of Scope
- strategy_id/key/params/active(이번 제외 — 인스턴스=다중 전략이나 할당 모델 portfolio-hub 미정의, 후속 discovery. loose JSON 미채택, 추후 명시 필드 additive 추가)
- intent admission/capacity 정책 자체(dcy-020 — read 표면만 변경)
- 포지션 사이징·실행 정책(dcy-021/022 — 별개)
- 전략 모듈(orb-v1) 내부 구현(외부 repo, 소비 계약만 합의)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High): **즉시 교체**(OQ-1, 과도기 없음)라 전략 모듈(외부 repo)이 같은 배포에 전환되지 않으면 전략이 스냅샷을 즉시 못 받는다.
- 완화 방안: 전략 모듈 lockstep cutover를 dcy-020 자산 중심 cutover(BAT-030, 이미 전략 계약 변경 동반)와 **단일 전환점으로 묶어** 조율. 배포 전 전략 모듈 `trex.slots.req` 대응 준비 확인.
- R-2 (Medium): 슬롯 행에 브로커 정본(qty/avg)과 hub 정본(cash_balance/asset_quantity)이 공존해 divergence(REQ-031/062) 시 소비자가 어느 값을 믿을지 모호.
- 완화 방안: 필드별 정본 출처·라벨을 OQ-3 에서 확정하고 계약 문서에 명시.
- R-3 (Medium): `available_slots` 제거로 전략이 capacity 사전 확인 불가 → 거부(`capacity_full`) 후 재시도 부담.
- 완화 방안: admission 게이트로 충분함을 합의(가정 A-3), 필요 시 별도 capacity 조회는 후속.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: dcy-020 자산 중심 영구 슬롯(슬롯=자산 1:1)이 구현 상태라 `assets[]`가 `slots[]`에 흡수 가능하다.
- A-2: (정정) 한 인스턴스 = **다중 전략**이다. 전략↔자산 할당은 **portfolio-hub 가 내려주는 미정의 설정**으로 별도 후속 discovery 에서 정의한다. 따라서 이번 read 표면은 전략 정보를 받지 않고(전략-무관), 전략 모델 확정 시 strategy 필드를 additive(비파괴)로 추가한다.
- A-3: capacity 사전 확인 없이 admission(`capacity_full`) 거부로 충분하다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: 큐 교체 방식 — `trex.slots.req` 신규 + `trex.snapshot.req` 즉시 폐기 vs 한시적 dual-listen/dual-publish 과도기.
- 상태: Answered (2026-06-11, 사용자 결정)
- 결정: **즉시 교체** — `trex.snapshot.req` 파이프라인 제거 + `trex.slots.req` 추가를 같은 배포에. dual-listen/dual-publish 과도기 **없음**. 전략 모듈(orb-v1)은 **lockstep cutover**(동시 전환)로 조율하며, dcy-020 자산 중심 cutover(BAT-030, 이미 전략 계약 변경 동반)와 함께 묶는 것을 권장. 반영: FR-1, NFR-3, R-1.
- OQ-2: `filter` 분류·조합 — status + overlay 혼합/단일, `inactive` 정의, `pending` buy/sell 합 여부.
- 상태: Answered (2026-06-11, 사용자 결정)
- 결정: `filter` = **status 단일 값**(상호배타) `{all, inactive, pending(=pending_buy∪pending_sell), holding, error}`. **status와 overlay는 직교 축** — overlay(`paused`/`warned`)는 필터 값이 아니라 **각 슬롯 행의 플래그**로 항상 노출, 서버측 overlay 필터는 후속(MVP는 행 플래그 + client 필터, "paused holding" 조합도 client에서). `inactive` 판정 키는 슬롯 **status**(hub `asset_quantity` 아님 — 상태머신 정본, 행에 asset_quantity 동반 노출). 임의 다중 status 조합 미지원(`pending` 묶음이 흡수). 반영: FR-2/3, §4.
- OQ-3: 병합 슬롯 행 필드·정본 — 수량 정본/미러 노출, divergence 표기, 워치독 형태.
- 상태: Answered (2026-06-11, 사용자 결정 (a))
- 결정: **(a) 둘 다 노출** — `asset_quantity`(hub 정본, REQ-062)와 `qty`(슬롯 미러)를 행에 함께, 어긋나면 `warned`. `cash_balance`(hub)+`cash_estimated`(dcy-018). 워치독 = **nested `{expired,last_seen}`**. overlay `paused`/`warned` 플래그. 라이브 브로커 잔고는 행 미포함(청산/reconcile 시점 정본). interface-contract 에 필드별 정본 출처 라벨 명시. 반영: FR-3, §4.
- OQ-4: strategy 필드 제외의 전제·처리.
- 상태: Answered (2026-06-11, 사용자 결정)
- 결정: 인스턴스는 **다중 전략**이나 전략↔자산 할당 모델이 **portfolio-hub 주도로 미정의** → **후속 discovery 로 연기**. 이번 표면은 전략 정보(strategy_id/key/params/active)를 **제외**(받지 않음). **loose JSON 필드는 미채택**(undefined blob은 OQ-3 정본 라벨 원칙에 반하고, 나중에 명시 필드로 additive 추가가 비파괴). 한계: 미래 모델 전까지 `trex.slots.req` 는 전략-무관(인스턴스 전체 슬롯, per-strategy 스코핑 없음). 반영: A-2, FR-6, §5 Out of Scope, 후속 Discovery 참조.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 전략이 `trex.slots.req {filter}` 로 슬롯 테이블을 받고, 응답에 `assets[]`/`available_slots`/`watchdog[]` 가 없으며 `slots[]`+`degraded`/`reason`/`message` 만 있다.
- S-2: `slots[]` 각 행이 슬롯 상태 + 자산(symbol/portfolio_id/cash_balance/asset_quantity) + 워치독(expired/last_seen)을 함께 담아, 별도 배열 조인 없이 소비된다.
- S-3: `spy=true` 조회는 워치독을 갱신하지 않고(생존 신호 미발생), 기본 조회는 holding 워치독을 터치한다. `filter=inactive` 가 매수 가능 목록을 반환한다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: `trex.slots.req` 응답 키 집합 단언(assets/available_slots/watchdog 부재) 단위/통합 / 데이터 출처: RPC 응답 payload
- S-2 측정 방식: 슬롯 행 필드 단언(자산·워치독 포함) / 데이터 출처: slot_to_dict 확장 결과
- S-3 측정 방식: spy on/off 워치독 갱신 여부 + filter=inactive 결과 단언 / 데이터 출처: WatchdogStore, 슬롯 상태
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 필터 분류(OQ-2)·strategy 제외 전제(OQ-4) 합의 완료
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~3 측정 시나리오·데이터 출처가 테스트 가능한 형태로 정의됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(큐 cutover)은 즉시교체(OQ-1) lockstep + dcy-020 cutover(BAT-030) 묶음으로 해소 방향 확정. 책임 = 구현 batch + 전략팀 조율.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~4 전부 Answered (2026-06-11 사용자 결정)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자 Justin confirm-discovery 수행
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-025_20260611_strategy-read-surface-slots-req.md
- 참조 문서:
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md (자산 중심 영구 슬롯·REQ-063 slots 단일 뷰 — 토대)
- docs/discovery/dcy-005_20260604_portfolio-hub-asset-snapshot-api.md (스냅샷 universe read 기원)
- docs/interface-contract.md(snapshot 계약), docs/runtime-flows.md (baseline — 갱신 대상)
- 코드: trex_core/application/portfolio.py(View/build_snapshot_envelope), messaging_handlers.py(snapshot 핸들러/_attach_watchdog)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- 없음 (OQ-1/2/3 Answered — §6 참조)
## CONFIRM
- 없음 (OQ-4 Answered — 인스턴스=다중 전략, 할당 모델은 후속 discovery, 이번 표면 전략 정보 제외)
## DATA
- 없음 (외부 데이터 의존 없음 — 내부 계약/토폴로지 + 전략 발신 합의)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~4 전부 Answered(즉시교체·filter=status 단일·행 정본(a)·전략 정보 제외). C형 플레이스홀더 0, 저장소 사실 재확인 일치(trex.snapshot.req·View enum·_attach_watchdog 존재). REQ Drafting 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin
- Confirmed At (KST): 2026-06-11 13:32 KST
@@ -1,246 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-026_20260612_sell-transaction-tax-and-fee-policy
- 이슈명: 매도 증권거래세 누락 + 수수료 정책 정정 (issue #24)
- 작성 시각(KST): 2026-06-12
- 마지막 갱신 시각(KST): 2026-06-12 (confirm-discovery: Justin Ko 승인 — 상태 confirmed 전환, §8 체크 완료, Freeze 플래그 갱신)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: 없음
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 점수 파일: 없음
- 관련 TODO/메모: GitHub issue #24 (`bug` 라벨, 2026-06-11 trex-admin-daily-trading 4건 체결 증거 포함)
- 해석:
- 매도 증권거래세가 전 체결에서 `tax=0.0`으로 저장되어 실현 손익·일일 수익률이 과대 계산되는 운영 버그. 코드상 `dcy-004 OQ-3`로 미뤄둔 "정밀 세금 모델"이 운영에서 표면화된 사안.
- 단순 핫픽스가 아니라 수수료율/세율 정책(매수·매도 구분, bp 단위, 세금 산식)을 계약으로 확정해야 하므로 신규 Discovery로 정식화한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- 기존 ledger 계열 Discovery(dcy-004 연동, dcy-015 outbox 백오프, dcy-018 cash write-back)는 fee/tax **정책 자체**를 다루지 않음. fee/tax 정확성은 미해결 영역.
- `trex_core/application/broker_port.py:51` 주석이 "tax defaults to 0 (precise tax model is a follow-up Discovery, dcy-004 OQ-3)"로 후속 Discovery 필요를 명시 — 그 후속을 본 Discovery로 개설.
# 3. 문제점의 요약
- 현재 상태
- `trex_core/application/broker_port.py:118-119`: `_FEE_BPS_BUY = _FEE_BPS_SELL = 1.5`(0.015%) — 수수료만 계산. `calculate_fee(side, amount)` 단일 함수.
- `trex_core/adapters/broker/paper.py:90`·`kis.py:213·230`: 체결 시 `tax=0.0` **하드코딩** — 매도 증권거래세가 전혀 반영되지 않음. 한국 주식 매도세(증권거래세+농특세 약 0.18~0.20%)는 수수료(0.015%)보다 10배 이상 큰 비용인데 누락.
- 결과(issue #24 증거): 2026-06-11 4건 체결의 저장 `fee+tax` 합계 177.9원 vs 실제 기대 1,357원 — 차액 대부분이 누락된 매도세.
- 기대 상태
- 매도 체결 시 증권거래세를 정책 기준으로 계산해 `FillStatus.tax`에 반영하고 `ledger_outbox`에 정확히 저장한다.
- 수수료율·세율을 단일 정책 지점(`calculate_fee`/`calculate_tax`)에서 관리하고, paper·KIS 어댑터가 동일 정책을 사용한다.
- 이해관계자
- 운영자/시스템 관리자 (Justin Ko): 정확한 일일 손익·수익률 리포트 필요
- portfolio-hub(ledger 수신측): `fee_amount`/`tax_amount` 정확값 수신
- 전략 모듈(trex-orb-v1): 비용 반영된 실현 손익 기반 의사결정
- 제약사항
- KIS 실 체결 응답에 수수료/세금 필드가 포함되면 우선 사용하고, 없으면 정책 추정값으로 폴백해야 함(브로커 정본 우선 원칙).
- 정책 상수는 코드 하드코딩 대신 설정 가능성을 검토(세율 변경 대응).
## 주요 사용자 시나리오
- 매도 round-trip 체결 후 운영자가 일일 리포트를 확인 → 수수료+세금이 실제 부과액과 일치하고 실현 손익이 과대 계산되지 않음.
- KIS 라이브 매도 체결 시 브로커 응답의 세금/수수료가 있으면 그 값이, 없으면 정책 추정값이 ledger에 기록됨.
- paper 브로커 검증 시에도 동일 정책으로 세금이 계산되어 라이브와 비교 가능한 ledger 엔트리 생성.
# 4. 이번에 정의할 변경
- 매도 증권거래세 산식 도입 — `calculate_tax(side, amount)`(매수 0, 매도 정책 세율) 추가 또는 `calculate_fee`를 fee+tax 동시 산출로 확장.
- paper·KIS 어댑터의 `tax=0.0` 하드코딩 제거 — 정책 함수 호출로 교체, KIS는 브로커 응답값 우선·정책 폴백.
- 수수료/세율 정책 상수를 env 설정으로 노출 (OQ-2 Closed) 및 baseline 문서(`runtime-flows.md`) fee/tax 흐름 갱신.
## 제비용 계산 공식 (OQ-1 Closed 기준)
정책 파라미터 (env 설정, 기본값):
```
TREX_KIS_FEE_BPS = 1.5 # 수수료율 (매수·매도 공통, 1.5bps = 0.015%)
TREX_KRX_TAX_SELL_BPS = 20.0 # KRX(KOSPI·KOSDAQ) 매도 증권거래세율 (20.0bps = 0.2%)
```
> 변수명에 `KIS`/`KRX` 도메인 라벨이 붙지만, 이 값은 단일 정책 지점으로 **paper·KIS 어댑터가 동일하게 공유한다**(FR-2·NFR-2). paper 브로커는 KIS 제비용을 모사하므로 같은 상수를 사용한다.
체결 단위 제비용:
```
amount = price × qty
fee = amount × TREX_KIS_FEE_BPS / 10_000 # 매수·매도 동일
tax = amount × TREX_KRX_TAX_SELL_BPS / 10_000 # 매도만 적용; 매수 = 0
total = fee + tax
```
side별 전개:
| side | fee | tax | total |
|------|-----|-----|-------|
| buy | amount × 0.00015 | 0 | amount × 0.00015 |
| sell | amount × 0.00015 | amount × 0.002 | amount × 0.00215 |
issue #24 역산 검증 (qty=1, OQ-1):
```
BUY 291,500원 → fee=43.73원, tax=0원, total=43.73원
SELL 301,000원 → fee=45.15원, tax=602원, total=647.15원
BUY 295,000원 → fee=44.25원, tax=0원, total=44.25원
SELL 298,500원 → fee=44.78원, tax=597원, total=641.78원
─────────────────────────────────────────────────────
합계 fee=177.9원, tax=1,199원, total=1,376.9원
(이슈 기재 1,357원과 차이 약 20원 — 이슈 원작성자 추정 오차)
```
## 영향 범위
- 코드 영향:
- `trex_core/application/broker_port.py`: `calculate_fee` 정정 + `calculate_tax` 신규(또는 통합), 세율/수수료율 상수
- `trex_core/adapters/broker/paper.py`: `tax=0.0` → `calculate_tax(...)` (line 90 주변)
- `trex_core/adapters/broker/kis.py`: `tax=0.0` → 브로커 응답 파싱 + 정책 폴백 (line 213·230 주변)
- `trex_core/application/ledger_outbox.py`·`execution.py`: fee/tax 전달 경로(계약 불변 가능성 높음, 검증 필요)
- 문서 영향:
- REQ-018(체결→원장 기록) change-req: fee/tax 산출 정책 명시
- REQ-011(BrokerPort + paper double) change-req 가능성: 수수료/세율 정책 함수 계약
- `docs/runtime-flows.md`: 체결→ledger fee/tax 산출 흐름
- 운영 영향:
- 배포 후 신규 체결부터 정확한 fee/tax 저장. **과거 잘못 저장된 행(2026-06-11 등) 보정 여부는 별도 결정**(OQ).
- portfolio-hub 측 손익 재계산 영향 — 수신측 정합 확인 필요.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: 매도 체결에 대해 정책 기준 증권거래세를 계산해 `FillStatus.tax`에 반영한다. 매수 체결의 세금은 0으로 한다(한국 주식 매수 무세 전제).
- FR-2: 수수료율·세율을 단일 정책 지점에서 관리하고 paper·KIS 어댑터가 동일 정책을 사용한다. `tax=0.0` 하드코딩을 제거한다.
- FR-3: KIS 어댑터는 브로커 체결 응답에 수수료/세금 필드가 있으면 그 값을 정본으로 사용하고, 없을 때만 정책 추정값으로 폴백한다.
- FR-4: `docs/runtime-flows.md`에 체결→ledger fee/tax 산출·저장 흐름을 갱신한다.
## 비기능 요구사항
- NFR-1: fee/tax 계산은 결정적이며 단위 테스트로 매수/매도 각 케이스의 기대값을 검증 가능해야 한다(issue #24 증거 수치 회귀 테스트 포함).
- NFR-2: paper·KIS 어댑터가 동일 입력(side, amount)에 동일 정책 결과를 산출한다(브로커 응답 미존재 시).
- NFR-3: 정책 변경(세율 조정) 시 코드 한 지점 수정으로 전 경로에 반영된다.
## 범위 경계
- In Scope
- 매도 증권거래세 산식 + 수수료율 정정
- paper·KIS `tax=0.0` 하드코딩 제거 및 정책 함수 적용
- KIS 브로커 응답 fee/tax 파싱(존재 시 정본)
- REQ-018(+REQ-011) change-req + runtime-flows 갱신 + 회귀 테스트
- Out of Scope
- 과거 잘못 저장된 ledger 행 소급 보정: **Out of Scope** — 보정하지 않음 (OQ-3 Closed)
- portfolio-hub 측 손익 재계산 로직 변경(수신측 저장소 책임)
- 세율의 동적 설정 UI/운영 도구(필요 시 후속)
# 6. 리스크/가정 목록
## 리스크
- R-1 (High): 세율 산식 오류 시 손익이 반대 방향으로 다시 왜곡됨 — 검증 부족하면 버그를 버그로 교체.
- 완화 방안: §4 공식 기준 기대 합계(fee 177.9 + tax 1,199 = **1,376.9원**)를 회귀 테스트 기준으로 고정. 매수/매도 각 케이스 단위 테스트. (이슈 기재 1,357원은 원작성자 추정 오차 약 20원 — baseline 아님.)
- R-2 (Medium): KIS 실 체결 응답의 수수료/세금 필드명·단위(원 vs bp)를 잘못 파싱하면 라이브에서만 오류 발생(paper에서 미검출).
- 완화 방안: KIS 응답 raw 필드 확인 후 파싱, 미확인 필드는 정책 폴백 + 로그. 라이브 스모크 시 실제 값 대조.
- R-3 (Medium): 과거 저장 행 미보정 시 2026-06-11 이전 리포트는 계속 왜곡 상태로 남음.
- 완화 방안: OQ-3에서 소급 보정 범위·방법 결정. 보정 안 하면 release 문서에 알려진 한계로 명시.
## 가정
- A-1: 한국 주식 기준 매수는 무세, 매도에 증권거래세(+농특세)가 부과된다. 정확한 세율 값은 사용자 확인 대상(OQ-1).
- A-2: 신규 REQ 없이 REQ-018(+필요 시 REQ-011) change-req로 닫힌다 — 계약 구조는 유지, 산식·값만 정정.
- A-3: KIS 체결 응답에 수수료/세금이 포함될 수 있으나 항상 보장되지는 않아 정책 폴백이 필요하다.
## 오픈 질문
- OQ-1: 적용할 정확한 매도 세율(증권거래세+농특세 합산 %)과 수수료율(매수/매도 bp)은?
- 상태: Closed
- 결정: 매도 증권거래세 **0.2%** (매수 무세), 수수료율 **0.015% (1.5bps)** 매수·매도 동일. issue #24 4건 체결 역산(qty=1 확인, 계산 기대 합계 1,376.9원)으로 현행 코드값 0.015% 정합 확인. 이슈 기재 1,357원은 약 20원 추정 오차.
- 종료 조건: 충족
- OQ-2: 정책 상수를 코드 상수로 둘지, 설정(env/TOML)으로 노출할지?
- 상태: Closed
- 결정: **env 설정**으로 노출한다. 확정 변수명:
- `TREX_KIS_FEE_BPS` — 한국투자증권 수수료율 (bps, 기본 `1.5` = 0.015%, 매수·매도 공통)
- `TREX_KRX_TAX_SELL_BPS` — KRX(KOSPI·KOSDAQ) 매도 증권거래세율 (bps, 기본 `20.0` = 0.2%)
- 기존 패턴(`TREX_WATCHDOG_TIMEOUT`, `TREX_PAPER_FILL_AFTER_POLLS` 등) 준용 — `TREX_{도메인}_{파라미터}` 형식
- 종료 조건: 충족
- OQ-3: 과거 잘못 저장된 ledger 행(2026-06-11 등) 소급 보정 여부와 방법은?
- 상태: Closed
- 결정: **소급 보정하지 않는다** — 과거 행은 그대로 둔다. 배포 후 신규 체결부터 정확한 fee/tax 저장. Out of Scope 확정.
- 종료 조건: 충족
# 7. 초기 성공 기준
- S-1: 매도 체결의 `tax`가 정책 세율로 계산되어 0이 아닌 정확값으로 `ledger_outbox`에 저장된다. issue #24의 4건 시나리오 재현 시 `fee+tax` 합계가 §4 공식 기대값(fee 177.9 + tax 1,199 = **1,376.9원**)에 정확히 수렴한다.
- S-2: paper·KIS 어댑터가 동일 정책으로 fee/tax를 산출하며, KIS는 브로커 응답값이 있으면 그 값을 사용한다.
- S-3: 기존 단위·통합 테스트 회귀 없음 + 신규 fee/tax 회귀 테스트 green, `ruff check` clean.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: 단위 테스트 — issue #24 4건 체결값(price/qty) 입력 후 `fee+tax` 합계가 §4 공식 기대 1,376.9원에 수렴 확인 / 데이터 출처: issue #24 Evidence + §4 공식 + `tests/application/` 신규 테스트
- S-2 측정 방식: paper·KIS 어댑터 단위 테스트로 동일 입력 동일 출력 확인 + KIS 응답값 우선 경로 테스트 / 데이터 출처: `tests/adapters/broker/`
- S-3 측정 방식: `pytest` 전체 회귀 + `ruff check trex_core tests` / 데이터 출처: 로컬/CI 결과
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 소급 보정(OQ-3) In/Out 확정 + 정책 상수 위치(OQ-2) 확정
- 결과: OQ-2 env 설정 확정, OQ-3 소급 보정 Out of Scope 확정
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: §4 공식 기대값 1,376.9원 역산에 필요한 세율/수수료율(OQ-1) 확정
- 결과: OQ-1 매도세 0.2%, 수수료 0.015% 확정 — §4 역산 합계 1,376.9원
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(세율 오류) 회귀 테스트 기준값 확정 담당 Justin Ko
- 결과: R-1 완화 방안 확정 — §4 기대 1,376.9원 회귀 테스트 기준 고정, Justin Ko 담당
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
- 결과: OQ-1·OQ-2·OQ-3 모두 Closed — Open 항목 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: Justin Ko 승인 후 `confirm-discovery` 수행
- 결과: Justin Ko 승인, confirm-discovery 완료
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md
- 참조 문서:
- GitHub issue #24 — fee/tax 저장 오류 증거(2026-06-11 4건 체결, 기대 1,357원 vs 저장 177.9원)
- trex_core/application/broker_port.py:51·118-131 — fee 모델 + tax follow-up 주석(dcy-004 OQ-3)
- trex_core/adapters/broker/paper.py:90, kis.py:213·230 — `tax=0.0` 하드코딩(변경 대상)
- docs/discovery/dcy-004_20260604_portfolio-hub-ledger-integration.md — OQ-3 정밀 세금 모델 선행 참조
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- 매도 세율: **0.2%**, 수수료율: **0.015% (1.5bps)** — 확정 (OQ-1 Closed)
- 정책 상수 위치: **env 설정** — `TREX_KIS_FEE_BPS` (기본 1.5), `TREX_KRX_TAX_SELL_BPS` (기본 20.0) — 확정 (OQ-2 Closed)
- 과거 ledger 행 소급 보정: **Out of Scope** — 확정 (OQ-3 Closed)
## CONFIRM
- KIS 체결 응답에 수수료/세금 필드가 실제 포함되는지 확인 — 포함 시 정본 우선, 미포함 시 정책 폴백(FR-3·R-2 연결). delivery 단계 KIS raw 응답 확인으로 종료.
## DATA
- 없음 — issue #24 4건 체결의 기대 수수료/세금 분해값은 §4 "제비용 계산 공식"에서 체결별로 제공됨(answered).
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: OQ-1(세율/수수료율), OQ-2(정책 위치), OQ-3(소급 보정) 3건 모두 Closed. §8 체크 완료. 구현 부재는 정상(신규 작업 정의 Discovery).
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-12
@@ -1,224 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-027_20260612_long-term-strategy-slot-operation
- 이슈명: 중장기 매매 전략용 슬롯 운영 도입 — 구현 위치 결정 + 멀티데이 리스크 규칙 + 운영 프로파일 (issue #20)
- 작성 시각(KST): 2026-06-12
- 마지막 갱신 시각(KST): 2026-06-12 (draft-req: REQ-076·077·078 생성 — 생성된 REQ 참조 갱신)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-076(watchdog timeout per-instance TOML), REQ-077(중장기 운영 프로파일), REQ-078(중장기 baseline 정합)
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 점수 파일: 없음
- 관련 TODO/메모: GitHub issue #20 (중장기 슬롯 운영 — 같은 프로젝트 내 신규 도메인 vs 별도 프로젝트 분리). dcy-020(자산 중심 영구 슬롯 — Out of Scope 로 "중장기 전용 운영의 전체"를 본 주기로 이월), dcy-024(safety_sell_enabled — "중장기 보유 인스턴스는 enabled=false" 용도로 설계), dcy-009(다중 인스턴스 토폴로지)와 직접 연계.
- 해석:
- issue #20 은 구 pool 모델(daily_slot·일일 preallocation·장 마감 강제 청산) 기준으로 "중장기와 정면 충돌"을 분석했으나, 이후 dcy-020/BAT-027~030 영구 자산 슬롯 cutover 와 dcy-024/BAT-036 `safety_sell_enabled=false` 출시로 충돌 지점 대부분이 이미 해소되거나 옵트아웃 가능해졌다 — 이슈의 전제를 현행 코드 기준으로 재정렬해야 한다.
- 본 Discovery 는 (1) 구현 위치(이슈 OQ-1)를 현행 모델 기준으로 재판정하고, (2) 잔여 갭 — safety-sell OFF 시 멀티데이 보호장치 부재, watchdog 의미 재검토, forced_close_at 재해석, 중장기 인스턴스 운영 프로파일 — 의 범위를 확정한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-020 이 issue #20 을 입력으로 참조했으나 Out of Scope 에 "중장기 전용 운영(issue #20)의 전체 — 본 모델은 토대만 제공"으로 명시적으로 제외했다. 그 이월 범위를 다루는 독립 주기가 없다.
- 멀티데이 리스크 규칙(트레일링/타임스톱)·중장기 인스턴스 운영 프로파일은 기존 어떤 REQ 도 다루지 않아 change-req 로 흡수할 대상 REQ 자체가 없다.
# 3. 문제점의 요약
- 현재 상태
- **이슈 #20 의 충돌 분석 중 이미 해소된 항목**: `daily_slot` 일일 리셋·preallocation 은 BAT-030 cutover 로 폐기됨(영구 자산 슬롯이 날 경계를 넘어 holding 유지 — `trex_core/domain/slot/`, `(instance_key, portfolio_id)` 키). `closed→idle` 재진입·`entry_count` 도 폐기됨(BAT-030/034). 장 마감 강제 청산·safety-sell 은 `safety_sell_enabled=false`(BAT-036, per-instance TOML)로 인스턴스 단위 옵트아웃 가능. 다중 인스턴스 토폴로지(dcy-009)·per-instance TOML(REQ-048)·config-dir(BAT-026)로 `instance_key=longterm` 독립 데몬 운영이 이미 가능.
- **잔여 갭 1 — 멀티데이 보호장치 부재**: `SafetySellService`(`trex_core/application/safety_sell.py`)의 stop-loss·forced_close·market-close 는 전부 safety-sell 워커에 묶여 있고 watchdog-gated(당일 전략 이탈 전제)다. `safety_sell_enabled=false` 인 중장기 인스턴스는 **어떤 코어 보호장치도 없다** — 멀티데이 stop-loss·트레일링 스톱·타임스톱이 존재하지 않는다.
- **잔여 갭 2 — watchdog 의미 충돌**: 워치독 timeout 기본 120s(REQ-034)는 분 단위 폴링 당일 전략 전제다. 하루 1~2회 체크하는 중장기 전략은 watchdog expired 가 일상 상태가 되어 가시화 신호로서 의미를 잃는다.
- **잔여 갭 3 — forced_close_at 의미**: 당일 청산 시각 전제 필드(REQ-032/037)로, 중장기에서는 타임스톱으로 재해석하거나 미사용 처리할지 미정이었음 → **미사용(의미 없음)으로 확정** (§10 CONFIRM, FR-4).
- **잔여 갭 4 — 운영 프로파일 부재**: 중장기 인스턴스용 표준 TOML 구성(safety_sell off + market-close 미설정 + watchdog 완화 + max_active_slots)이 문서화·검증된 적 없다. baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재·최신이나 당일 운영 중심으로 기술돼 있다.
- 기대 상태
- 구현 위치 확정 완료 — **(a′) 기존 슬롯 도메인 + 중장기 운영 프로파일 추가** (OQ-1 Answered). 신규 `PositionBook` 도메인 불필요.
- 보호 규칙 경계 확정 완료 — **"전략 모듈 전적 책임"으로 명시적으로 선을 긋는다** (OQ-2 Answered). 코어는 운영자 CLI `exit` 백스톱만 제공하며, 이 경계가 운영 문서에 명시된다.
- 중장기 인스턴스 운영 프로파일(TOML 레시피 + watchdog 정책)이 baseline 문서에 기술되고 실 데몬으로 검증된다.
- 이해관계자
- 운영자/시스템 관리자 (Justin Ko): 당일·중장기 인스턴스 병행 운영, 리스크 정책 결정
- 중장기 전략 모듈(신규 — trex-orb-v1 와 별개): intent 발신·슬롯 read 소비자
- 기존 당일 인스턴스: 회귀 없이 무간섭 운영 유지
- 제약사항
- 검증된 당일 경로(BAT-027~036)에 회귀를 주지 않아야 한다 — 이슈도 "당일 슬롯 머신에 멀티데이 의미를 끼워 넣으면 조건 분기 폭증" 위험을 지적.
- 플랫폼 계층(브로커/ledger/transport/slots RPC/CLI)은 호라이즌 무관으로 공유한다 — 분리 시 인프라 복제 손해(이슈 분석 유지).
- 중장기 인스턴스는 별도 `instance_key` 로 큐·DB 스코프를 분리해 당일 인스턴스와 무간섭이어야 한다(dcy-009 토폴로지 전제).
## 주요 사용자 시나리오
- 운영자가 `instance_key=longterm` 데몬을 중장기 프로파일 TOML 로 기동 → 매수 후 슬롯이 며칠~수개월 holding 을 유지하고, 장 마감·자정·재시작을 넘어도 청산되지 않는다.
- 중장기 전략 모듈이 하루 1회 `trex.slots.req` 로 보유 현황을 읽고 리밸런싱 sell/buy intent 를 발신 → watchdog 가 이를 비정상으로 오인하지 않는다.
- 보유 중 종목이 손절 기준을 하회 → (코어 규칙 제공 시) 멀티데이 stop-loss 가 발화하거나, (전략 책임 시) 전략 모듈이 sell intent 로 청산한다 — 어느 쪽인지 정책이 명확하다.
- 당일 인스턴스와 중장기 인스턴스가 같은 PostgreSQL·RabbitMQ 인프라에서 병행 운영되어도 슬롯·큐·정책이 간섭하지 않는다.
# 4. 이번에 정의할 변경
- 구현 위치 확정 — **(a′) 기존 슬롯 도메인 유지 + 중장기 운영 프로파일/규칙 추가** (OQ-1 Answered). 현행 영구 자산 슬롯이 멀티데이 보유를 구조적으로 지원하므로 신규 `PositionBook` 도메인(이슈 원안 (a))·별도 레포((c))는 미채택. 이 판정이 REQ 분할의 전제다.
- 멀티데이 보호 규칙 경계 확정 — **전략 모듈 전적 책임** (OQ-2 Answered). `safety_sell_enabled=false` 인스턴스는 코어 손절매 루틴(stop-loss·트레일링·타임스톱)을 일절 사용하지 않고, 코어는 운영자 CLI `exit` 백스톱만 제공한다. 이 경계를 운영 문서에 명시한다(FR-2).
- 중장기 인스턴스 운영 프로파일 정의 — 표준 TOML 레시피(safety_sell off·market-close 미설정·watchdog per-instance timeout(OQ-3: TOML `watchdog_timeout_sec`, 중장기 1800s 이상)·max_active_slots) + baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`) 중장기 운영 흐름 갱신.
## 영향 범위
- 코드 영향 (구현 위치 (a′) 확정 기준 — OQ-1):
- `trex_core/runtime/config.py` — 중장기 프로파일 설정: per-instance TOML `watchdog_timeout_sec`(OQ-3, 현재 env `TREX_WATCHDOG_TIMEOUT` 전용 → TOML 우선 지원)
- watchdog timeout per-instance 적용 경로(`trex_core/application/watchdog.py` + config wiring)
- 멀티데이 리스크 워커/`safety_sell.py` 변경 **없음** — OQ-2 결정(코어 손절매 루틴 미제공, 전략 전적 책임)으로 코어 보호 규칙 구현이 범위에서 제외됨
- 당일 경로(`domain/slot/`, `intents.py`, `execution.py`)는 불변 목표
- 문서 영향:
- `docs/project-structure.md`·`docs/runtime-flows.md`: 중장기 인스턴스 운영 흐름·프로파일 추가 (baseline 갱신)
- 신규 REQ 다수 예상(운영 프로파일·리스크 규칙) + REQ-048(TOML 설정)·REQ-034(watchdog) change-req 가능성
- 운영 영향:
- 신규 `instance_key=longterm` 데몬 추가 기동(별도 TOML)— 기존 당일 인스턴스 무중단
- 멀티데이 보유 포지션의 일일 회계·리포트 해석 변화(실현손익은 청산일 귀속)
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: 중장기 인스턴스 운영 프로파일을 정의한다 — 표준 TOML 레시피(safety_sell_enabled=false, market-close 미설정, watchdog 정책, max_active_slots)와 기동 검증 절차를 문서화하고 실 데몬으로 확인한다.
- FR-2: `safety_sell_enabled=false` 인스턴스는 코어 손절매 루틴을 일절 사용하지 않는다. 모든 보호 규칙(stop-loss·트레일링·타임스톱)은 전략 모듈의 전적 책임이며, 코어는 운영자 CLI `exit` 백스톱만 제공한다. 이 경계를 운영 문서에 명시한다.
- FR-3: watchdog 의 중장기 의미를 재정의한다 — per-instance timeout 설정 또는 중장기 인스턴스 비활성화 옵션 중 확정안을 적용해, 저빈도 폴링 전략이 expired 로 오인되지 않게 한다.
- FR-4: `forced_close_at` 은 중장기 인스턴스에서 **미사용(의미 없음)** 으로 확정한다 (CONFIRM 2026-06-12). 집행 주체(safety-sell 워커)가 OFF 이므로 구조적으로 발화 불가 — 운영자 CLI·intent 계약 문서에 "중장기 인스턴스에서 미사용"을 명시한다.
- FR-5: `docs/project-structure.md`·`docs/runtime-flows.md` 에 중장기 인스턴스 운영 구조와 흐름(병행 인스턴스 토폴로지, 프로파일, 리스크 규칙 경로)을 갱신한다.
## 비기능 요구사항
- NFR-1: 당일 인스턴스 경로 무회귀 — 기존 단위·통합 테스트 전부 green, 당일 운영 TOML 의 동작 변화 0건.
- NFR-2: 인스턴스 간 무간섭 — 중장기 인스턴스의 슬롯·큐·정책 변경이 당일 인스턴스 DB/큐 스코프에 영향을 주지 않음을 통합 테스트로 검증 가능해야 한다.
- NFR-3: 프로파일 설정은 기존 per-instance TOML 메커니즘(REQ-048)을 재사용하고 새 설정 채널을 만들지 않는다.
## 범위 경계
- In Scope
- 구현 위치 판정(이슈 OQ-1 재평가) 및 REQ 분할 전제 확정
- 중장기 인스턴스 운영 프로파일(TOML 레시피 + watchdog 정책 + 기동 검증)
- 멀티데이 보호 규칙 경계 문서화 — **전략 모듈 전적 책임** + 운영자 CLI exit 백스톱 명시(코어 구현 없음)
- forced_close_at 중장기 의미 확정
- baseline 문서(project-structure·runtime-flows) 중장기 운영 갱신
- Out of Scope
- 중장기 전략 모듈 자체의 구현(별도 저장소/주기 — 코어는 계약·운영 토대만 제공)
- 포트폴리오 리밸런싱 엔진·목표 비중 관리(전략 모듈 책임)
- 일일 회계·리포트 체계 개편(현행 ledger 날짜 필터 해석 유지, 필요 시 후속)
- 신규 `PositionBook` 도메인 신설 — **Out of Scope 확정** (OQ-1: 구현 위치 (a′) 결정으로 미채택)
# 6. 리스크/가정 목록
## 리스크
- R-1 (High): 보호장치 경계가 모호한 채 중장기 인스턴스를 운영하면, safety-sell OFF + 전략 모듈 장애 시 손실 포지션이 무기한 방치된다.
- 완화 방안: **전략 모듈 전적 책임으로 확정(OQ-2)**. 코어는 운영자 CLI `exit` 백스톱만 제공. 보호 규칙 공백 위험은 전략 모듈 설계 및 운영 절차(운영자 수동 CLI)로 완화한다.
- R-2 (해소됨 — OQ-2): 멀티데이 규칙을 기존 safety-sell 워커에 얹으면 당일 경로와 조건 분기가 얽혀 회귀 위험(이슈가 경고한 "분기 폭증")이 생긴다.
- 해소 근거: OQ-2 결정으로 코어가 멀티데이 보호 규칙을 구현하지 않으므로 safety-sell 워커 변경 자체가 없다. 당일 경로 테스트 전체는 NFR-1 무회귀 게이트로 유지.
- R-3 (Medium): watchdog 완화가 과도하면 중장기 인스턴스에서 전략 이탈을 영영 감지하지 못한다.
- 완화 방안: 비활성화 대신 긴 timeout(예: 일 단위) 설정을 기본안으로 검토하고, slots RPC `watchdog.last_seen` 가시성은 유지한다.
- R-4 (Low): 멀티데이 보유 중 universe 에서 자산이 빠지면(상폐 등) tombstone/retire 경로가 중장기 보유와 충돌할 수 있다.
- 완화 방안: dcy-020 의 universe-detached holding(tombstone) 경로가 이미 존재 — 중장기 시나리오로 검증 항목에 포함.
## 가정
- A-1: 현행 영구 자산 슬롯(BAT-027~030)은 멀티데이 보유를 구조적으로 이미 지원한다 — 슬롯 키에 날짜가 없고 holding 이 날 경계·재시작을 넘는다(dcy-020 OQ-3 결정).
- A-2: 중장기 운영은 별도 `instance_key` 데몬으로 분리한다(이슈 OQ-3 의 권장 방향) — 단일 데몬 내 호라이즌 혼합은 다루지 않는다. **(승인 확정 — 2026-06-12, Justin Ko, §10 CONFIRM)**
- A-3: 플랫폼 계층(브로커/ledger/transport/slots RPC/CLI)은 변경 없이 공유 가능하다 — 이슈의 재사용 분석 유지.
- A-4: KIS·portfolio-hub·RabbitMQ 인프라를 그대로 사용하므로 별도 레포 분리(이슈 OQ-1 의 (c))는 손해다 — 이슈 분석 수용.
## 오픈 질문
- OQ-1: 구현 위치 최종 확정 — (a′) 기존 슬롯 도메인 + 프로파일/규칙 추가 vs (a) 신규 PositionBook 도메인 병렬 신설. 현행 영구 슬롯이 멀티데이를 이미 지원하므로 (a′) 가 유력하나, 중장기 전용 수명주기 요구(트레일링 상태, 리밸런싱 이력 등)가 슬롯 도메인을 오염시킬 수준인지 판단 필요.
- 상태: Answered (2026-06-12, Justin Ko)
- 결정: **(a′) 기존 슬롯 도메인 유지 + 운영 프로파일/규칙 추가**. 현행 영구 자산 슬롯이 멀티데이 보유를 구조적으로 이미 지원하므로(날짜 없는 슬롯 키, holding 날 경계 통과) 신규 PositionBook 도메인 신설은 불필요. `{{DECIDE_POSITION_BOOK}}` 은 Out of Scope 확정.
- 반영 위치: `# 5` 범위 경계({{DECIDE_POSITION_BOOK}} Out of Scope 확정), `# 10` DECIDE 항목.
- OQ-2: 멀티데이 보호 규칙의 코어 제공 범위 — 멀티데이 stop-loss / 트레일링 스톱 / 타임스톱(forced_close_at 재해석) 중 코어가 구현할 항목과 전략 모듈 전적 책임으로 둘 항목은?
- 상태: Answered (2026-06-12, Justin Ko)
- 결정: **`safety_sell_enabled=false` 인스턴스는 코어 손절매 루틴을 일절 사용하지 않는다.** stop-loss·트레일링 스톱·타임스톱 등 모든 보호 규칙은 전략 모듈의 전적 책임이다. 코어는 운영자 CLI `exit` 백스톱(수동 청산)만 제공한다. R-1(방치 위험)은 전략 모듈 설계 책임 + 운영 절차(운영자 CLI) 로 완화한다.
- 반영 위치: `# 5` FR-2(멀티데이 보호 규칙 경계), `# 6` R-1 완화 방안 갱신, `# 10` DECIDE 항목.
- OQ-3: watchdog 중장기 정책 — per-instance timeout 연장(일 단위) vs 중장기 인스턴스 비활성화 옵션 중 어느 쪽인가?
- 상태: Answered (2026-06-12, Justin Ko)
- 결정: **per-instance timeout 연장**. OQ-4 최대 폴링 주기(10분/600s) 기준으로 per-instance TOML `watchdog_timeout_sec` 설정을 지원하고, 중장기 인스턴스는 1800초(30분) 이상으로 설정한다. `last_seen_at` 가시성은 유지한다.
- 알림 후속 주석: watchdog 소유권 진입(전략 이탈 감지) 시 사용자에게 외부 알림(예: 슬랙·이메일 등)을 발송하는 기능을 추후 별도 Discovery 로 구현할 예정이다. 현재 주기에서는 로그 경고(`watchdog ownership entered`) 수준으로 유지하고 알림 채널 연동은 범위 밖이다.
- 반영 위치: `# 5` FR-3(watchdog 재정의), `# 10` DECIDE 항목.
- OQ-4: 중장기 전략 모듈의 폴링·intent 발신 주기 전제 — 하루 몇 회 슬롯 read/intent 발신을 가정하는가? (watchdog·프로파일 수치의 입력)
- 상태: Answered (2026-06-12, Justin Ko)
- 결정: **최소 1분, 최대 10분 주기**로 `trex.slots.req` 호출 및 intent 발신. 빠른 경우 1분 간격, 느린 경우 최대 10분(600s) 간격을 전제.
- 반영 위치: OQ-3 watchdog timeout 수치 입력 — 10분 폴링 대비 충분한 여유(권장: 30분 이상).
# 7. 초기 성공 기준
- S-1: `instance_key=longterm` 프로파일 TOML 로 기동한 데몬에서 매수→day 경계 통과→보유 유지가 확인된다(장 마감·재시작 후에도 holding 유지, 자율 청산 미발화).
- S-2: 보호 규칙 경계가 운영 문서에 명시된다 — `safety_sell_enabled=false` 인스턴스의 모든 보호 규칙(stop-loss·트레일링·타임스톱)은 전략 모듈 전적 책임(OQ-2), 코어는 운영자 CLI `exit` 백스톱만 제공. 백스톱 경로(CLI exit → 청산)가 중장기 인스턴스에서 동작함이 확인된다.
- S-3: 당일 인스턴스 전체 테스트 무회귀 + 중장기 인스턴스와의 무간섭(슬롯·큐 스코프)이 통합 테스트로 검증된다.
- S-4: baseline 문서(project-structure·runtime-flows)에 중장기 운영 구조·흐름이 반영되어 이후 REQ/batch 가 참조 가능하다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: 실 데몬 run-the-app 검증 — longterm TOML 기동 후 paper 매수, 시스템 시각/일자 경계 시나리오로 holding 유지 확인 / 데이터 출처: `run-trex-core-daemon` skill 경로 + 데몬 로그 + `trex-core slots`
- S-2 측정 방식: 운영 문서(baseline) diff 리뷰 — 보호 규칙 경계(전략 책임 + CLI 백스톱) 명시 확인 + 중장기 프로파일 인스턴스에서 `trex-core exit` 백스톱 paper 동작 확인 / 데이터 출처: `docs/runtime-flows.md` + 데몬 로그·`trex-core slots`
- S-3 측정 방식: `pytest` 전체 회귀 + instance_key 격리 통합 테스트(기존 `test_instance_topology.py` 패턴 확장) / 데이터 출처: 로컬/CI 결과
- S-4 측정 방식: baseline 문서 diff 리뷰 — 중장기 흐름 섹션 존재 + 현행 코드와 정합 / 데이터 출처: `docs/project-structure.md`, `docs/runtime-flows.md`
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1(구현 위치 a′)·OQ-2(보호 규칙 전략 책임) Answered + A-2(병행 인스턴스) 승인 + forced_close_at 미사용 확정으로 충족 — PositionBook Out of Scope 확정. CONFIRM 잔여 0건
- 결과: 충족 — In/Out 경계 §5 확정, 사용자 결정 잔여 0건
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: OQ-4 수치(1~10분 폴링, watchdog 1800s 이상) 확보로 S-1/watchdog 검증 수치 구체화 완료
- 결과: 충족 — S-1~S-4 측정 방식·데이터 출처 §7 명시
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(보호장치 공백) — OQ-2 결정으로 경계 확정(전략 책임 + CLI 백스톱), 담당 Justin Ko
- 결과: 충족 — R-1 경계 확정, R-2 해소(OQ-2), R-3/R-4 완화 방안 확정
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~4 전부 Answered — `Open` 항목 0개
- 결과: 충족 — Open 0건
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: §10 CONFIRM 2건 해소 + Justin Ko 승인 후 `confirm-discovery` 수행
- 결과: 충족 — Justin Ko 승인, confirm-discovery 수행 완료
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-027_20260612_long-term-strategy-slot-operation.md
- 참조 문서:
- GitHub issue #20 (closed — 본 Discovery 로 이월) — 중장기 슬롯 운영 도입, 호라이즌 결합 분석 + OQ-1~4 (구 pool 모델 기준 — 본 문서 §3 에서 현행 기준 재정렬)
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md — 영구 자산 슬롯 토대(Out of Scope 로 본 주기 이월 명시), OQ-3 trading_date 폐기 결정
- docs/discovery/dcy-024_20260611_safety-sell-disable-mode.md — safety_sell_enabled per-instance TOML ("중장기 보유 인스턴스는 enabled=false" 용도 명시)
- trex_core/application/safety_sell.py — 당일 전제 보호장치(watchdog-gated stop-loss/forced_close/market-close), 멀티데이 규칙 부재 확인
- trex_core/runtime/config.py — per-instance TOML(REQ-048)·market_close_liquidation_time·watchdog timeout 설정 현황
- stage-pilot/templates/project-structure.md, stage-pilot/templates/runtime-flows.md — baseline 갱신 시 논리 템플릿 참조
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- ~~{{DECIDE_IMPLEMENTATION_LOCATION}}~~: 구현 위치 — **(a′) 기존 슬롯 도메인 + 운영 프로파일/규칙 추가** 확정 (OQ-1, 2026-06-12, Justin Ko)
- ~~{{DECIDE_RISK_RULES}}~~: 멀티데이 보호 규칙 — **전략 모듈 전적 책임** 확정. `safety_sell_enabled=false` 시 코어 손절매 루틴 미제공, 운영자 CLI exit 백스톱만 유지 (OQ-2, 2026-06-12, Justin Ko)
- ~~{{DECIDE_WATCHDOG_POLICY}}~~: watchdog 중장기 정책 — **per-instance timeout 연장** 확정 (OQ-3, 2026-06-12, Justin Ko). 알림 연동은 후속 Discovery.
- ~~{{DECIDE_POSITION_BOOK}}~~: PositionBook 도메인 신설 — **Out of Scope 확정** (OQ-1 결정, 2026-06-12, Justin Ko)
## CONFIRM
- ~~{{CONFIRM_PARALLEL_INSTANCE}}~~: 중장기 운영을 **별도 instance_key 데몬으로 분리** — 승인 확정 (A-2, 2026-06-12, Justin Ko). 단일 데몬 내 호라이즌 혼합은 다루지 않으며, dcy-009 토폴로지·REQ-048 per-instance TOML 전제 위에서 병행 운영한다.
- ~~{{CONFIRM_FORCED_CLOSE_SEMANTICS}}~~: forced_close_at 의 중장기 의미 — **미사용(의미 없음) 확정** (FR-4, 2026-06-12, Justin Ko). 중장기 모드에서 forced_close_at 은 의미가 없다. 집행 주체(safety-sell 워커)가 `safety_sell_enabled=false` 로 OFF 이므로 구조적으로 발화 불가하며, 타임스톱 재해석(별도 집행 경로 신설)은 OQ-2 결정(코어 손절매 미제공)과 충돌해 미채택. 운영자 CLI·intent 계약 문서에 "중장기 인스턴스에서 미사용" 명시.
## DATA
- 없음 — 중장기 전략 모듈의 폴링·intent 발신 주기는 OQ-4 에서 확보됨(answered): 최소 1분~최대 10분(600s) 주기, watchdog timeout 권장 1800초(30분) 이상의 입력값.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1(구현 위치 a′)·OQ-2(보호 규칙 전략 전적 책임)·OQ-3(watchdog timeout 연장)·OQ-4(폴링 주기 1~10분) 4건 모두 Answered. CONFIRM 2건도 확정 — A-2(별도 instance_key 분리) 승인 + forced_close_at 중장기 미사용(의미 없음). 사용자 결정 잔여 0건. 구현 부재는 정상(신규 작업 정의 Discovery — 코어 변경은 watchdog per-instance TOML 정도로 경량).
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-12
@@ -1,234 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-028_20260615_slot-strategy-state-store
- 이슈명: Slot Strategy-State 저장소 (per-strategy opaque state store on trex-core slots, issue #26 C-1/C-2/C-3)
- 작성 시각(KST): 2026-06-15 16:26
- 마지막 갱신 시각(KST): 2026-06-15 18:05
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-079, REQ-080, REQ-081, REQ-082 (+ change-req: REQ-073, REQ-074)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #26 (trex-tsl strategy-daemon 팀 계약 제안), C-4 확인 코멘트·OQ 결정 코멘트(게시 완료), trex-tsl 팀 occupancy race 피드백(#4705990067)
- 해석:
- trex-tsl(매도 전략 데몬)이 TSL trailing stop line 계산에 필요한 **시간 누적 상태(예: entry 이후 peak price)**를 슬롯 수명에 묶어 server-side로 보관할 generic state store를 요청했다(C-1/C-2/C-3).
- 이번 Discovery는 "trex-core가 해석하지 않는 opaque state를 `(slot_id, strategy_id)` 키로 저장/조회/수명관리"하는 범위를 정의한다. OQ-1~4 전부 Answered. trex-tsl 팀이 점유 전환 ABA 누수(High)를 지적해 occupancy_seq 경량 가드(C-2b)를 v1 범위로 추가, full 3계층 분할만 후속 Deferred. C-4(SellIntent/watchdog)는 기존 계약 확인 항목으로 답변 완료.
- trex-tsl-v1 운영 모드 = **intraday, `safety_sell_enabled=true`**(core SL/TP watchdog backup + market_close EOD backstop 유지, 데몬이 primary 매도). 중장기 프로파일과 무관 — core가 backup으로 남으며 당일 재진입이 잦아 occupancy 가드 필요성이 큼.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- `docs/discovery/` 전체에서 `strategy_state`/per-strategy opaque store를 다루는 기존 Discovery가 없다(grep 결과 0건). 가장 가까운 dcy-025(strategy read surface — slots.req 노출면)는 슬롯 메타 read 확장이지 strategy 소유 상태 저장/CAS write/수명 삭제 계약이 아니다.
- C-2(version CAS write RPC)·C-3(close 시 삭제 불변식)는 신규 RPC와 신규 도메인 저장 개념을 도입하므로 기존 REQ/batch 산출물의 미세 조정 범위를 넘어선다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 슬롯 스냅샷 RPC(`trex.slots.req`)는 `trex_core/application/messaging_handlers.py`의 `build_slots_handler` + `slot_to_dict`(`trex_core/application/portfolio.py:250`)로 슬롯 행을 구성하며, strategy 소유 임의 상태를 실어 나르는 필드가 없다.
- 슬롯 도메인(`trex_core/domain/slot/models.py` `Slot`, `state.py`)에는 position/occupancy 필드만 있고 strategy opaque blob 저장소가 없다. 전략은 `evaluate()` 순수성 때문에 상태를 스스로 영속화할 수 없다.
- 슬롯 close는 영구 슬롯 모델(dcy-020/REQ-057, BAT-030)에서 **행 삭제가 아니라 `→ INACTIVE` 전이**다(`trex_core/domain/slot/service.py:174` — position/occupancy 필드 일괄 clear, asset 정체성 보존). 현재 이 전이에서 정리할 strategy_state 자체가 없다.
- 결과적으로 전략 데몬이 peak 등 누적 상태를 데몬 메모리에 두면 재시작/장애/failover 시 유실되어 stop line이 느슨해지는 실손실 위험이 있고, 별도 외부 스토어를 쓰면 orphan-cleanup 책임이 발생한다.
- 기대 상태
- `(slot_id, strategy_id)`로 네임스페이스된 opaque state를 trex-core가 저장하고, 슬롯 스냅샷 응답에 round-trip(추가 왕복 없이 read), occupancy_seq + version CAS로 idempotent write, 슬롯 close(`→INACTIVE`) 시 자동 삭제한다.
- 전략/데몬은 cleanup 책임이 없고, 슬롯이 source of truth이자 GC가 된다. 재사용된 슬롯은 새 occupancy_seq + 빈 상태(version 없음)로 시작한다.
- 이해관계자
- trex-tsl 매도 전략 데몬 팀 (제안자/주 소비자)
- trex-core 유지보수자 (계약·구현·검증 책임)
- 운영 담당자 (멀티데이 인스턴스 운영, dcy-027 long-term 프로파일 연계)
- 제약사항
- trex-core는 `data`를 절대 파싱/검증/해석하지 않는다(opaque). schema는 전략 소유.
- 순수 AMQP 계약 통합만 — 데몬은 `trex_core`를 직접 import 하지 않는다.
- 단일 writer 전제(해당 slot·strategy를 운용하는 데몬 1개). version CAS는 재시도/중복 안전용이지 다중 writer 동시성 제어가 아니다.
- 영구 슬롯 모델 정합: 삭제는 행 삭제가 아니라 `→INACTIVE` 전이 훅에서 수행한다(dcy-020).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- TSL 데몬이 매 decision-cycle에 `trex.slots.req`로 슬롯과 함께 `strategy_state["tsl_v1"]`를 읽어 `evaluate()`의 read-only 입력으로 쓰고, 갱신된 peak 상태를 `trex.slots.strategy_state.set`(expected_version CAS)로 write 한다.
- 데몬이 재시작/장애로 메모리 상태를 잃어도 다음 사이클에 슬롯 스냅샷에서 peak를 복원해 stop line이 느슨해지지 않는다.
- 슬롯이 매도 체결로 close(`→INACTIVE`)되면 trex-core가 해당 슬롯의 모든 strategy_state를 삭제하고, 같은 슬롯이 재사용되면(occupancy_seq++) 전략은 빈 상태에서 다시 시작한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- C-1: 슬롯 스냅샷 RPC(`trex.slots.req`) 응답 각 슬롯 행에 optional `strategy_state` 맵을 추가(additive, 하위호환). 미보유 시 omit 또는 `{}`.
- C-2: 신규 RPC `trex.slots.strategy_state.set` — `(slot_id, strategy_id)` opaque value를 version CAS로 upsert. 기존 계약 패턴(direct reply-to RPC + JSON + `queue_name({base}.{instance_key})`) 준수.
- C-2b (occupancy_seq 경량 가드): 슬롯 행에 monotonic `occupancy_seq`(새 점유 시작 시 ++)를 추가하고 snapshot 행에 노출한다. `strategy_state.set`은 `expected_occupancy_seq`를 받아 **occupancy_seq 일치 + version 일치**를 함께 CAS 검증, 점유가 바뀌었으면 `occupancy_changed` reject. 점유 전환(read v0→close→reopen→write v0)의 ABA 누수를 구조적으로 차단(intraday 재진입 안전). full 3계층(occupancy 테이블 분할)은 후속 Deferred — 본 가드는 컬럼 1개로 정합성 임계부만 선취.
- C-3: 슬롯 close 시 strategy_state 삭제 불변식 — 영구 슬롯 모델 정합상 `→INACTIVE` 전이(`service.py:174`)에서 해당 slot_id의 모든 strategy_state 제거.
- (baseline) `docs/runtime-flows.md`에 strategy-state read/write/삭제 흐름 추가, `docs/project-structure.md`에 저장소 구조·계약 키 반영.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/messaging_handlers.py`(`build_slots_handler` 응답에 strategy_state + occupancy_seq 병합, 신규 strategy_state.set 핸들러 + occupancy_seq CAS)
- `trex_core/application/portfolio.py`(`slot_to_dict`에 strategy_state + occupancy_seq round-trip)
- 신규 저장 포트/리포지토리(예: `StrategyStateStore`)와 PG/in-memory 구현(`trex_core/adapters/persistence/postgresql.py`, `trex_core/domain/slot/in_memory_repository.py` 또는 별도 모듈)
- `trex_core/domain/slot/models.py`·`service.py`(슬롯에 `occupancy_seq` 필드 추가 + 새 점유 시작(`inactive→pending_buy`) 시 ++ + `→INACTIVE` 전이 시 strategy_state 삭제 훅), 스키마 마이그레이션(`occupancy_seq` 컬럼) 및 데몬 와이어링(`trex_core/runtime/daemon.py`)
- 문서 영향:
- `docs/runtime-flows.md`(strategy-state 흐름), `docs/project-structure.md`(저장소/계약 키), `docs/srs/index.md`(신규 REQ 등록)
- 운영 영향:
- 신규 RPC 라우팅 키/큐 추가 → 데몬·인프라 설정 반영. PG 사용 시 신규 테이블/스키마 마이그레이션.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1 (C-1): `trex.slots.req` 응답 각 슬롯 행에 optional `strategy_state` 맵(`{<strategy_id>: {version:int, data:str, updated_at:str}}`)과 **`occupancy_seq`(int)**를 추가한다. 미보유 시 omit/`{}`. 기존 소비자는 unknown 필드 무시로 하위호환. 맵은 별도 `slot_strategy_state` 테이블(OQ-4)에서 반환 slot_id들로 batched 조회 후 group by 해 조립한다(슬롯 본체 스키마 불변; occupancy_seq는 슬롯 행 컬럼).
- FR-2 (C-2): 신규 RPC `trex.slots.strategy_state.set`로 `(slot_id, strategy_id)` opaque `data`를 **occupancy_seq + version CAS** upsert 한다. `expected_occupancy_seq`가 슬롯 현재 occupancy_seq와 일치하고 `expected_version`이 stored version과 일치 → commit 후 version+1. 점유 불일치 → `{ok:false, reason:"occupancy_changed", occupancy_seq:<cur>}` (저장 안 함; 데몬 re-read 후 fresh restart). version 불일치 → `{ok:false, reason:"version_conflict", version:<cur>}`. slot inactive/missing → `{ok:false, reason:"slot_inactive"}` (저장 안 함). 최초(없음 + expected_version=0, occupancy_seq 일치) → version=1 생성. 동일 `correlation_id` 재전송은 부작용 없음. `data`는 opaque UTF-8 문자열로 받아 (1) 유효 UTF-8 + (2) 크기 상한(NFR-2/OQ-1)만 검증하고 그대로 저장하며, 초과 시 `{ok:false, reason:"state_too_large"}`로 reject 한다(저장 안 함). malformed 페이로드는 `{ok:false, reason:"bad_request:<detail>"}`. **reject reason 전체 집합 = {`occupancy_changed`, `version_conflict`, `slot_inactive`, `state_too_large`, `bad_request`}**, 데몬은 미인식 reason에 fail-closed.
- FR-2b (C-2b, occupancy_seq 가드): 슬롯에 monotonic `occupancy_seq` 필드를 두고 **새 점유 시작(`inactive→pending_buy`) 시 ++**, snapshot 행(FR-1)에 노출한다. read v0→close→reopen→write v0 의 ABA 누수를 FR-2 occupancy CAS로 구조적으로 차단한다. (full occupancy 테이블 분할은 Out of Scope — 컬럼 1개로 정합성 임계부만 선취.)
- FR-3 (C-3): 슬롯이 어떤 경로로든 close(`→INACTIVE`)되면 해당 slot_id의 모든 strategy_state를 삭제한다. 재사용 슬롯은 새 occupancy_seq + 빈 상태(version 없음)로 시작한다. 삭제 즉시성 통지는 **신규 이벤트를 만들지 않고**(OQ-3 확정), 데몬은 다음 `slots.req` 스냅샷의 state 부재(또는 occupancy_seq 변화)로 감지한다; 즉시성이 필요하면 이미 발행되는 `slot_filled_sell`(status=inactive) transition 이벤트를 재사용한다(필요시 `strategy_state_cleared` additive 플래그).
- FR-4 (baseline): `docs/runtime-flows.md`에 strategy-state read/write/삭제 흐름·occupancy_seq 가드·영구 슬롯 close 훅 정합을, `docs/project-structure.md`에 저장소 구조·계약 키를 반영한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1 (불간섭/opaque): trex-core는 `data`를 파싱/검증/해석하지 않는다. 충돌/만료/해석은 전적으로 trex-core 밖이다.
- NFR-2 (인코딩/크기 한계, OQ-1 확정): `data`는 core가 해석하지 않는 **opaque UTF-8 문자열**이다 — JSON/base64를 계약 레벨에서 강제하지 않으며(base64 기본 강제는 ~33% 팽창·무이득), core 검증은 (1) 유효 UTF-8 + (2) 크기 상한(byte 기준) 두 가지뿐이다. 상한은 **2단** — TSL 등 알려진 용례 기본 **1~2KB**, **hard ceiling 4KB**. 초과 시 명시적 reject(`state_too_large`). C-1 read piggyback상 스냅샷 응답 크기 ≈ `max_active_slots × cap` 이므로 상한은 슬롯당 저장이 아니라 폴링 응답 크기 관점에서 정한다.
- NFR-3 (수명 정합): strategy_state는 슬롯 수명 동안만 존재하고 장기 영속/감사 대상이 아니다. read는 기존 스냅샷 RPC에 round-trip 되어 추가 왕복이 없고 watchdog 갱신을 깨지 않는다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- C-1 read 확장(strategy_state 맵 + occupancy_seq), C-2 write RPC, C-2b occupancy_seq 경량 가드, C-3 close 삭제 불변식, baseline 문서 정합
- occupancy_seq + version CAS 의미(occupancy_changed/version_conflict/slot_inactive/state_too_large/bad_request/first-create/idempotent)
- Out of Scope
- 다중 writer 동시성 제어(단일 writer 전제)
- strategy_state 장기 보존/감사/이력 저장(별도 논의)
- C-4(SellIntent/watchdog) 신규 구현 — 기존 계약 확인 항목으로 issue #26 코멘트에서 답변 완료
- **full 3계층 슬롯 구조 분할**(별도 `asset_slot`/`slot_occupancy` 테이블, strategy_state를 occupancy_id 키로 전환) — 후속 "영구 슬롯 구조 정합" Discovery로 Deferred. `daily_slot` 네이밍 정합·`daily_symbol_entries` 잔재 제거도 함께. (정합성 임계부인 occupancy_seq 가드는 본 batch 포함 — C-2b. 중장기 영구 슬롯엔 점유 회전이 드물어 full 분할은 과함; 가드 컬럼 1개로 충분.)
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (High→완화): 점유 전환 ABA 누수 — read v0→close→reopen→write v0 시 X가 계산한 상태가 새 점유 Y에 바인딩(잘못된 stop line, 안전 관련). version CAS(0 vs 0)·slot_inactive(write 시 Y로 이미 active)로는 못 잡음. intraday Mode A(당일 재진입)에서 실재(trex-tsl 팀 #26 지적).
- 완화 방안: **occupancy_seq 경량 가드(C-2b/FR-2b)** — 새 점유마다 occupancy_seq ++, write가 `expected_occupancy_seq`로 점유 일치를 server-side CAS 검증 → 다른 점유면 `occupancy_changed` reject. ABA를 구조적으로 차단. + `→INACTIVE` 단일 전이 지점 삭제 훅(모든 close 경로 커버)·테스트로 보강.
- R-2 (Medium): write RPC race/중복 전달로 상태 덮어쓰기.
- 완화 방안: occupancy_seq + version CAS + `correlation_id`("{slot_id}:{strategy_id}:{expected_version}") idempotency. 단일 writer 전제 문서화.
- R-3 (Low): PG 스키마 추가/마이그레이션 운영 부담(별도 `slot_strategy_state` 테이블 + 슬롯 `occupancy_seq` 컬럼).
- 완화 방안: opaque blob 별도 테이블 + occupancy_seq 컬럼 1개의 최소 스키마(JSONB-on-row는 OQ-4에서 기각). in-memory 구현 병행으로 paper/테스트 무중단.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1 (OQ-4로 확정): strategy_state는 별도 `slot_strategy_state` 테이블 + 맵(복합 PK `(slot_id, strategy_id)`) + slot_id 키. 슬롯 본체(`daily_slot`, 레거시 명 — 실제는 영구 자산 슬롯)에는 정합성 가드용 `occupancy_seq` 컬럼 1개만 추가(full 테이블 분할 없음).
- A-2 (OQ-2로 확정): read는 기존 `trex.slots.req`에 piggyback, write는 별도 신규 RPC `trex.slots.strategy_state.set`. (가정 → 확정)
- A-3: 영구 슬롯 모델(dcy-020) 정합상 삭제는 행 삭제가 아니라 `→INACTIVE` 전이 훅이다(issue #26 코멘트에서 데몬 팀에 이미 고지).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1 (OQ-A): `data` 인코딩을 JSON 문자열로 할지 base64 바이트로 할지, 수용 가능한 크기 상한(제안 4KB)은 얼마인지.
- 상태: Answered (2026-06-15, Justin Ko)
- 결정: **opaque UTF-8 문자열 + 2단 상한**. JSON/base64를 계약 레벨에서 강제하지 않고 core는 유효 UTF-8 + byte 크기 상한만 검증(NFR-1 정합). 상한은 TSL 등 알려진 용례 기본 1~2KB, hard ceiling 4KB(byte 기준). 초과 시 `state_too_large` 명시적 reject. C-1 스냅샷 응답 크기(`max_active_slots × cap`) 관점에서 상한을 정한다.
- 반영 위치: NFR-2(인코딩/크기 한계), FR-2(write 검증·reject 사유).
- OQ-2 (OQ-B): 신규 RPC(`trex.slots.strategy_state.set`) vs 기존 메시지(heartbeat/intent) piggyback 중 어느 write 방식을 채택할지.
- 상태: Answered (2026-06-15, Justin Ko)
- 결정: **write = 별도 RPC `trex.slots.strategy_state.set`(C-2), read = `trex.slots.req` piggyback(C-1) 유지**. piggyback 기각 근거 — (1) write(거의 매 사이클)와 sell intent(드묾)의 빈도 불일치, (2) trex-core에 dedicated heartbeat 부재 + watchdog는 `slots.req`가 모든 holding 슬롯에 매 사이클 갱신해야 해 조건부 write와 합치면 커버리지 붕괴, (3) read RPC에 write를 얹으면 멱등성·`spy` 의미 붕괴, (4) CAS 응답/재시도 격리, (5) trex-core가 이미 concern별 개별 RPC 패턴이라 추가 비용 낮음. read는 어차피 폴링하는 `slots.req`에 공짜로 얹히므로 비대칭(read piggyback / write 분리)이 의도된 설계.
- 반영 위치: FR-1(C-1 read piggyback), FR-2(C-2 별도 write RPC). A-2 가정 → 확정.
- 비고: 사이클당 다중 슬롯 write 시 N개 set RPC 발생 — optional batch 변형은 v1 범위 밖 후속 최적화.
- OQ-3 (OQ-C): close 시 strategy_state 삭제를 slot-transition 이벤트(`trex.events.strategy.*`)로 노출할지(데몬 즉시 반응 가능).
- 상태: Answered (2026-06-15, Justin Ko)
- 결정: **신규 전용 이벤트 도입하지 않음.** 정합성은 (1) 스냅샷 부재(`slots.req`에서 strategy_state 없음 → 데몬 `expected_version=0` first-create) + (2) C-2 CAS rule(닫힌 슬롯 write는 `slot_inactive` reject로 부활 차단)가 보장한다. 즉시성이 필요하면 **이미 발행되는 `slot_filled_sell`(status=inactive) transition 이벤트를 재사용**(모든 매도 close가 execution loop `pending_sell→INACTIVE` 경로로 이 이벤트를 emit; best-effort). 정 필요하면 그 payload에 `strategy_state_cleared: true` **additive 필드**만 추가 — 새 event_type/route 아님. 전용 state-cleared 이벤트는 구체적 필요 발생 시 후속 반복으로 Deferred.
- 근거: 정합성이 best-effort 이벤트에 의존하지 않도록(source of truth = 스냅샷), 신규 event_type의 라우팅/바인딩/DLQ 비용 회피, transition 이벤트·스냅샷 부재와의 중복 제거.
- 반영 위치: FR-3(삭제 불변식 — 이벤트 노출은 옵션, 기존 transition 이벤트 재사용 명시), NFR-3(수명 정합).
- OQ-4 (OQ-D): `strategy_state`를 슬롯당 다중 strategy 맵으로 둘지 단일 객체로 둘지 + 저장 모델(별도 테이블 vs 슬롯 행 JSONB).
- 상태: Answered (2026-06-15, Justin Ko)
- 결정: **별도 `slot_strategy_state` 테이블 + 맵 + `slot_id` 키.** ① shape = 맵(strategy_id 네임스페이스, 계약 키 `(slot_id, strategy_id)`로 강제) — 한 슬롯이 `strategy_id`(매수)·`sell_strategy_id`(매도)를 동시 보유 가능하므로 단일 객체는 충돌. ② 저장 = 별도 테이블(JSONB-on-row 기각): CAS가 `UPDATE ... WHERE version=?` rowcount / `INSERT ON CONFLICT`로 race-safe, hot 슬롯 행(`daily_slot`) lean 유지, write 경합 분리, C-3 삭제 `DELETE WHERE slot_id=?` 한 문장. 테이블 PK = 복합 `(slot_id, strategy_id)`, strategy당 1행. C-1 read는 반환 slot_id들로 batched 조회 후 group by 해 맵 조립. in-memory repo는 복합키 dict로 미러. **참조 방향은 strategy_state → slot(테이블이 slot_id를 컬럼으로 보유), slot 본체 스키마 불변.**
- 키 선택(slot_id vs occupancy_id): **strategy_state 테이블은 slot_id 키 유지 + occupancy_seq 경량 가드(C-2b) 추가.** (2026-06-15 갱신, trex-tsl #4705990067 지적 반영) 당초 "occupancy 식별자 전면 deferred"는 점유 전환 ABA 누수(R-1)를 못 막음이 확인되어 수정 — 정합성 임계부인 occupancy 식별만 **슬롯 행 occupancy_seq 컬럼 1개**로 v1에 선취하고, write CAS가 `expected_occupancy_seq`로 점유 일치를 검증한다. **full 3계층(별도 occupancy 테이블 + strategy_state를 occupancy_id 키로 전환)만 Deferred** — 중장기 영구 슬롯은 점유 회전이 드물어 full 분할이 과하고, intraday(trex-tsl)가 요구하는 정합성은 가드 컬럼으로 충족. strategy_state 격리 테이블이라 추후 full 분할 시 seq→id 흡수도 contained.
- 반영 위치: FR-1(read 맵 조립), FR-2(CAS 저장), FR-2b/C-2b(occupancy_seq 가드, In Scope), FR-3(close 삭제), §4 영향 범위(별도 StrategyStateStore/테이블), 범위 경계(full 3계층만 Out of Scope), A-1 확정.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 데몬이 `trex.slots.req`로 슬롯과 함께 strategy_state·occupancy_seq를 read하고, `trex.slots.strategy_state.set`으로 write한 뒤, 같은 슬롯의 다음 스냅샷에서 version+1과 갱신된 data가 보인다.
- S-2: CAS 의미(occupancy match+version match commit / version_conflict / occupancy_changed / slot_inactive / state_too_large / first-create)와 idempotent 재전송 무부작용이 테스트로 검증된다.
- S-3: 슬롯이 모든 close 경로(전략 매도 체결/safety-sell/forced-close)로 `→INACTIVE` 전이 시 strategy_state가 삭제되고, 재사용 슬롯이 새 occupancy_seq + 빈 상태(version 없음)로 시작한다.
- S-4: 점유 전환 ABA 시나리오(read v0 → close → reopen(occupancy_seq++) → 옛 점유 기준 write) 가 `occupancy_changed`로 reject 되어 새 점유에 stale 상태가 바인딩되지 않음이 테스트로 검증된다.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: read/write/재read end-to-end 통합 테스트 / 데이터 출처: messaging_handlers 핸들러 테스트, slots.req 응답 페이로드
- S-2 측정 방식: strategy_state.set 핸들러 단위 테스트(6 케이스: occupancy+version commit/version_conflict/occupancy_changed/slot_inactive/state_too_large/first-create + 중복 재전송) / 데이터 출처: 신규 핸들러/스토어 테스트
- S-3 측정 방식: 각 close 경로 후 strategy_state 부재 + occupancy_seq 증가 검증 테스트 / 데이터 출처: slot service `→INACTIVE`/`inactive→pending_buy` 전이 테스트, 스토어 상태
- S-4 측정 방식: 점유 전환 race 단위 테스트(occupancy_seq mismatch → occupancy_changed) / 데이터 출처: strategy_state.set 핸들러/스토어 테스트
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: C-1/C-2/C-2b/C-3 + baseline 포함, C-4·다중writer·장기보존·full 3계층 분할 제외 합의 — confirm-discovery 확정.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 테스트 가능 형태로 확정 (§7 측정 방식 참조)
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(점유 전환 ABA, High) → occupancy_seq 가드(C-2b/FR-2b)로 완화안 채택 확정. R-2/R-3 Medium/Low.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
- 결과: OQ-1~4 전부 Answered (2026-06-15). full 3계층 분할만 후속 Discovery로 Deferred(occupancy_seq 가드는 v1 포함).
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: confirm-discovery 시 승인자 지정 — Justin Ko (2026-06-15)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-028_20260615_slot-strategy-state-store.md
- 참조 문서:
- GitHub issue #26 (Slot Strategy-State 계약 제안) + C-4 확인 코멘트 + OQ 결정 코멘트 + occupancy race 피드백/답변(#4705990067, #4706137866)
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md (영구 슬롯/→INACTIVE 모델)
- docs/discovery/dcy-025_20260611_strategy-read-surface-slots-req.md (slots.req read surface)
- docs/discovery/dcy-027_20260612_long-term-strategy-slot-operation.md (영구 슬롯/long-term 프로파일 연계)
- stage-pilot/templates/project-structure.md, stage-pilot/templates/runtime-flows.md
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- ~~`data` 인코딩 + 크기 상한 — OQ-A~~ → **해결됨(2026-06-15)**: opaque UTF-8 문자열 + 2단 상한(기본 1~2KB / hard ceiling 4KB), 초과 시 `state_too_large` reject. (OQ-1 참조)
- ~~write 경로(별도 RPC vs piggyback) — OQ-B~~ → **해결됨(2026-06-15)**: write = 별도 RPC(C-2), read = slots.req piggyback(C-1) 유지. (OQ-2 참조)
- ~~슬롯당 다중 strategy 맵 vs 단일 객체 + 저장 모델(테이블 vs JSONB) — OQ-D~~ → **해결됨(2026-06-15)**: 별도 `slot_strategy_state` 테이블 + 맵(복합 PK `(slot_id, strategy_id)`) + slot_id 키, occupancy_id 후속 Deferred. (OQ-4 참조)
## CONFIRM
- ~~close 시 strategy_state 삭제를 slot-transition 이벤트로 노출할지 — OQ-C~~ → **해결됨(2026-06-15)**: 신규 이벤트 없음, 스냅샷 부재 + CAS 의존, 기존 `slot_filled_sell` 옵션 재사용. (OQ-3 참조)
- 삭제 훅을 영구 슬롯 `→INACTIVE` 전이로 두는 설계(행 삭제 아님) — A-3·FR-3로 확정, 데몬 팀 고지 완료. confirm-discovery 승인 시 함께 확정.
## DATA
- ~~PG 컬럼/메시지 크기 기준 실수용 가능한 data 상한 값~~ → **확정(OQ-1)**: 기본 1~2KB / hard ceiling 4KB(byte 기준)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~4 전부 Answered(2026-06-15) — 인코딩(opaque UTF-8+2단 상한)/write 경로(별도 RPC)/삭제 이벤트(신규 없음)/저장 구조(별도 테이블+맵+slot_id 키 + occupancy_seq 경량 가드) 확정. trex-tsl 팀 점유 ABA 지적 반영(C-2b). full 3계층 분할만 후속 Discovery로 Deferred. 범위·성공기준·저장소 사실관계 확인 완료, C유형 placeholder·Open OQ 0개 → REQ 초안 작성 진행 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-15 17:52
@@ -1,237 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-029_20260616_operational-observability-logging-and-pnl-view
- 이슈명: 운영 관측성 — 주문 결과 로깅 + 로그 타임스탬프 + 청산사유 분류 + 슬롯/PnL 조회 + 운영자 런북 (issue #29)
- 작성 시각(KST): 2026-06-16 10:52
- 마지막 갱신 시각(KST): 2026-06-16 12:55
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-084, REQ-085, REQ-086, REQ-087, REQ-088, REQ-089, REQ-090
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #29 (P0: 1·2 / P1: 3·4·5), 관련 issue #28(호가단위 거부 — 별도 정확성 버그 트랙)
- 해석:
- 2026-06-15/16 라이브 테스트에서 trex-core의 운영 가시성 부족이 진단을 막았다. 매수 거부(#28)가 로그에 없고 `slot_transition_log` DB에만 남아 수 시간 동안 인지하지 못했고, 로그에 타임스탬프가 없어 타임라인 재구성이 불가능했다.
- 이번 Discovery는 "거래 실패를 즉시·정확히 관측"하기 위한 로깅/분류/조회 표면을 정의한다. 청산사유 분류·조회 뷰가 런타임 흐름과 read 표면을 바꾸므로 minor-change가 아니라 정식 Discovery로 다룬다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 Discovery(dcy-001~028)는 슬롯 도메인·전송·브로커·안전매도·자산중심 슬롯·전략 read 표면을 다뤘고, "운영 관측성(로깅/타임스탬프/청산사유 분류/PnL 조회)"을 정면으로 다룬 주기는 없다. dcy-016/dcy-018(슬롯 상태 가시화·운영자 CLI)은 슬롯 상태 조회에 한정돼 주문 결과 로깅·PnL 집계와 범위가 다르다.
- issue #29는 신규 운영 환류(라이브 테스트 인사이트)에서 출발한 독립 주제로, 후속 반복을 독립 주기로 추적하는 편이 책임 경계가 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- 로그 포맷: `trex_core/runtime/daemon.py:408`·`trex_core/runtime/reset.py:129`가 `logging.basicConfig(level=logging.INFO)`만 호출해 **format 문자열이 없다** → 모든 로그 라인에 타임스탬프·레벨 prefix가 없다.
- 주문 결과 가시성: `trex_core/application/execution.py`의 submit/poll 경로는 산발적 `_log.debug`/`_log.warning`만 남기고, 주문 submit + 결과(체결가/수량/수수료/세금 또는 거부+사유)를 일관된 tailable 로그로 남기지 않는다. `_rollback()`은 거부 시 슬롯을 **조용히** 전이(`slot_transition_log`에만 기록)하고 ERROR 로그를 남기지 않아 호가단위 거부(#28)가 로그에 보이지 않았다.
- 청산사유 분류: `trex_core/application/safety_sell.py`·`trex_core/application/watchdog.py` 양쪽이 매도를 일으키는데, watchdog 청산이 `stop_loss`로 뭉뚱그려져 "진짜 손절"로 오해를 유발했다(2026-06-15 064400 사례).
- 조회 표면: 운영자 CLI(`trex_core/runtime/cli.py`)는 `slots/universe/pause/enter/exit/cancel`만 제공하고, 보유/오늘 체결/실현 PnL(수수료·세금 반영)/대기 주문을 한눈에 보는 뷰가 없다. 일일 실현 PnL을 `ledger_outbox`에서 수기 계산해야 했다. 데몬은 RabbitMQ 전용(BAT-007)이라 HTTP 표면이 없다.
- baseline 문서(`docs/project-structure.md`, `docs/runtime-flows.md`)는 존재하며 최신(2026-06-15 갱신). 단, 로깅/관측성·PnL 조회 흐름은 아직 기술돼 있지 않다.
- 기대 상태
- 모든 로그 라인에 ISO8601(KST) 타임스탬프 + 레벨이 붙고, 모든 주문 submit/결과/거부가 타임스탬프 달린 일관 로그(Loki 적재용 구조화 JSON + correlation id)로 즉시 관측된다.
- 청산이 기존 reason 어휘(`stop_loss`/`take_profit`/`forced_close_at`/`market_close_liquidation`)와 actor(`safety_sell`/`strategy`/`execution_loop`)를 함께 노출해, "안전망 자율청산"과 "전략 손절"이 구분 기록된다.
- 운영자가 보유·오늘 체결·실현 PnL(수수료·세금 반영)·대기 주문을 단일 CLI 명령으로 조회할 수 있어 일일 리포트 자동화의 기반이 된다.
- 주문 거부가 조용히 묻히지 않고 ERROR 로그로 즉시 인지된다(알림 전송 채널은 후속 반복으로 Deferred).
- 이해관계자
- 운영 담당자(trex 운영팀): 실패를 즉시 인지하고 일일 리포트를 자동화해야 하는 1차 수요자.
- 문서 작성자(AI agent): Discovery/REQ/Batch 산출물 작성·구현·검증.
- 리뷰어/승인자(Justin Ko): 범위·계약·우선순위 확정.
- 제약사항
- 데몬은 HTTP 표면이 없다(BAT-007, RabbitMQ-only). "조회 endpoint"는 운영자 CLI 확장 또는 RabbitMQ RPC로 구현해야 하며 HTTP 신설은 아키텍처 역행이다.
- 청산사유 reason 값은 전략/운영자가 읽는 계약 표면(`slots.req`, 이벤트, `slot_transition_log`)에 노출되므로 어휘 변경 시 하위호환을 고려해야 한다.
- 코어는 알림 전송 채널의 1차 소유자가 아니다(ERROR 로그는 코어, 전송은 운영 인프라/외부).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자가 거래일 중 `tail -f`로 trex-core 로그를 보며 매수 주문이 호가단위 오류로 거부되는 것을 **즉시** 발견하고(타임스탬프+ERROR), #28 같은 문제를 몇 시간이 아니라 즉시 인지한다.
- 운영자가 장 마감 후 단일 명령/RPC로 "오늘 체결 N건, 실현 PnL(수수료·세금 반영) X원, 현재 보유 M종목, 대기 주문 K건"을 조회해 일일 리포트를 자동 생성한다.
- 운영자가 슬롯 청산 기록을 보고 그것이 watchdog 강제청산인지 진짜 손절(stop_loss)인지 사유로 즉시 구분한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 로깅 기반(P0): 전 로그 라인에 ISO8601(KST) 타임스탬프 + 레벨 prefix 부여, 모든 주문 submit/결과(체결가·수량·수수료·세금 또는 거부+사유)를 타임스탬프 달린 일관 로그로 출력, Loki 적재용 구조화(JSON) 출력 + correlation id(intent→order→fill) 전파.
- 의미론·가시성(P1): 청산을 기존 reason 어휘 + actor를 함께 노출해 안전망/전략을 구분 기록(신규 필드 없음), 주문 거부 시 조용한 롤백 대신 ERROR 로그(알림 전송 채널은 Deferred).
- 조회 표면(P1): 보유·오늘 체결·실현 PnL(수수료·세금 반영)·대기 주문 조회 뷰를 운영자 CLI 서브커맨드로 추가(DB-backed, RPC 없음).
- baseline 문서 갱신: `docs/runtime-flows.md`에 로깅/관측성·청산사유 분류·PnL 조회 흐름을 추가하고, 신규 모듈(로깅 포맷/주문결과 로깅/PnL 집계)이 생기면 `docs/project-structure.md`에 책임을 반영.
- 운영자 런북 문서(P1): `docs/operations.md` 신설 — 배포/실행·운영자 CLI 사용법·로깅/관측성 사용법·디버깅/트러블슈팅. `run-trex-core-daemon` SKILL의 사실 자료 추출 + 본 반복 신설 관측성 기능 사용법 통합.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/runtime/daemon.py`·`trex_core/runtime/reset.py`: 로깅 포맷(타임스탬프+레벨, KST) 설정.
- `trex_core/application/execution.py`: 주문 submit/결과/거부 일관 로깅, `_rollback()` 거부 시 ERROR 로그.
- `trex_core/application/safety_sell.py`·`trex_core/application/watchdog.py`(+ `trex_core/domain/slot/service.py` reason 전파): 청산사유 분류.
- 신규/확장: 주문결과 로깅 헬퍼, PnL 집계 read(`ledger_outbox`/체결 집계), 조회 CLI 서브커맨드(`trex_core/runtime/cli.py`).
- 구조화 출력(Loki JSON) + correlation id(intent→order→fill) 전파 (FR-7, In Scope).
- 문서 영향:
- `docs/runtime-flows.md`(로깅/관측성·PnL 조회 흐름 추가), `docs/project-structure.md`(신규 모듈 책임), `docs/interface-contract.md`(조회 CLI·청산사유 reason+actor 어휘·거부 로그 계약).
- 운영 영향:
- 로그 포맷 변경으로 기존 로그 파싱(있다면) 조정 필요. 조회 뷰/PnL 집계로 일일 리포트 자동화 가능. (알림 채널은 이번 범위 밖.)
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1 (P0): 모든 로그 라인에 ISO8601 타임스탬프(KST) + 로그 레벨 prefix를 부여한다(현재 prefix 없음).
- FR-2 (P0): 모든 주문 submit과 그 결과(체결가·체결수량·수수료·세금, 또는 거부+사유)를 타임스탬프 달린 일관(tailable) 로그 라인으로 출력한다. 호가단위 같은 거부가 즉시 보여야 한다.
- FR-3 (P1): 청산 사유를 **두 축**으로 정확히 구분 노출한다(OQ-4 결정 = Option A: 신규 필드 없음). (1) 조건축 = 기존 `reason` 어휘 `{stop_loss, take_profit, forced_close_at, market_close_liquidation}`를 계약으로 고정, (2) 주체축 = 기존 `actor`(`safety_sell`=안전망 watchdog 소유 자율청산 vs `strategy`=전략 자율매도 vs `execution_loop`)를 reason과 **함께** 로그/`slot_transition_log`/조회 뷰에 노출. 현재 stop_loss/take_profit/forced_close_at는 모두 watchdog expired(전략 침묵→안전망 소유)에서만 발화하므로, actor를 같이 보여야 "안전망이 SL선에서 청산"과 "전략이 손절"을 구분할 수 있다(2026-06-15 064400 오해 원인). 신규 reason 값·신규 분류 필드는 추가하지 않는다.
- FR-4 (P1): 주문 거부 시 조용한 inactive/holding 롤백 대신 ERROR 레벨 로그를 남긴다(알림 전송 채널은 Deferred, OQ-3). 반복 거부를 즉시 인지할 수 있어야 한다.
- FR-5 (P1): 현재 보유·오늘 체결·오늘 실현 PnL(수수료·세금 반영)·대기 주문을 조회하는 운영 뷰를 **운영자 CLI 서브커맨드**로 제공한다(OQ-1 결정). 조회는 `ledger_outbox`/slots를 권위 출처로 한 DB-backed 시점 집계이며, RPC는 추가하지 않는다. 일일 리포트 자동화의 기반.
- FR-6: 이 변경이 런타임 흐름/구조를 바꾸므로 `docs/runtime-flows.md`(로깅/관측성·PnL 조회 흐름)와 필요 시 `docs/project-structure.md`(신규 모듈 책임)를 갱신한다.
- FR-7 (P0): 로그를 **Loki 적재용 구조화(JSON) 출력**으로 내보내고, correlation id(intent→order→fill)를 전파해 한 거래의 인텐트·주문·체결 로그를 단일 키로 묶는다(OQ-2 결정, 이번 범위 In Scope). FR-1·2·3·4의 로그가 이 구조화 스트림으로 흐른다.
- FR-8 (P1, Documentation): 운영자용 런북 문서(`docs/operations.md`)를 신설한다. 배포/실행(env 변수·`--config`·`--env-file`), 운영자 CLI 사용법(`pause/resume/slots/universe/enter/exit/cancel` + 본 반복 신설 PnL/조회 서브커맨드), 로깅·관측성 사용법(타임스탬프·주문결과 로그 grep·Loki/correlation id·청산 reason+actor 해석), 디버깅·트러블슈팅(거부 ERROR 확인, 미체결 진단, DB 상태 조회)을 포함한다. 기존 `.claude/skills/run-trex-core-daemon/SKILL.md`의 사실 자료를 사람용 문서로 추출·정리하고, FR-1~7로 추가되는 관측성 기능의 운영 사용법을 함께 기술한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1 (운영성): 로그 타임스탬프는 KST 기준 ISO8601로 일관되며, 주문 결과 로그는 `grep`/`tail`로 거부·체결을 즉시 필터링할 수 있는 안정적 키(예: 종목·사유·intent_id)를 포함한다.
- NFR-2 (하위호환): 청산사유 노출 방식 개선(기존 reason 어휘 고정 + actor 동반 노출, 신규 필드 없음)과 조회 표면 추가는 기존 `slots.req`/이벤트/CLI 동작을 깨지 않는 additive 변경을 원칙으로 한다.
- NFR-3 (정확성): PnL 집계는 BAT-038(매도 증권거래세+수수료) 정책과 일치하는 수수료·세금 반영 값을 사용하며 `ledger_outbox`/체결 기록을 단일 출처로 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- FR-1·FR-2(P0 로깅 기반), FR-3·FR-4(P1 분류·거부 가시성), FR-5(P1 조회 뷰), FR-6(baseline 문서 갱신), FR-7(P0 Loki 구조화 출력 + correlation id), FR-8(P1 운영자 런북 `docs/operations.md`).
- FR-5 조회 표면 = 운영자 CLI 서브커맨드(DB-backed). RPC는 추가하지 않음(OQ-1 결정).
- FR-7 Loki 구조화(JSON) 출력 + correlation id(intent→order→fill) 전파를 이번 범위에 포함(OQ-2 결정).
- Out of Scope
- issue #28(호가단위 tick-size 스냅) — 별도 정확성 버그 트랙. 본 Discovery는 그 거부를 "관측"하는 범위만 다룬다.
- FR-5 조회용 RabbitMQ RPC 표면 — 이번 반복 제외(CLI로 충분, OQ-1 결정).
- 거부 시 알림 "전송 채널" 구현(claude-bus/외부 webhook 등) — Deferred. 이번 범위는 FR-4의 ERROR 로그까지만(OQ-3 결정). 후속 반복/이슈로 인계.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Low): 청산사유 노출 방식 변경(FR-3)이 reason 계약을 바꿔 다운스트림 소비자(trex-tsl/trex-orb, 운영 리포트)에 영향을 줄 수 있다. (OQ-4=Option A로 신규 reason 값·필드 없음 → 리스크 Low로 하향)
- 완화 방안: 기존 reason 값 유지(어휘 고정만), actor는 이미 존재하는 필드를 함께 노출하는 additive 변경. `docs/interface-contract.md`에 reason+actor 어휘를 명문화, 표시 방식 변경은 issue 코멘트로 전략팀 통지.
- R-2 (Medium): 로그 포맷 변경(FR-1)이 기존 로그 수집/파싱 파이프라인(있다면)을 깨거나, KST 타임스탬프 도입이 컨테이너 TZ 설정에 의존해 불일치할 수 있다.
- 완화 방안: TZ는 코드에서 명시적으로 KST(Asia/Seoul) 변환, 환경 TZ 비의존. 포맷 변경 영향은 운영팀과 사전 합의.
- R-3 (Low): PnL 집계(FR-5)가 `ledger_outbox`/체결 기록의 부분 상태(미정산·재시도 중)와 수수료·세금 정책 차이로 수기 계산과 어긋날 수 있다.
- 완화 방안: BAT-038 정책과 동일 계산 지점 재사용, "오늘" 경계는 trading_date(KST) 기준, 집계 시점의 outbox 상태를 명시.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 조회 표면은 HTTP가 아니라 운영자 CLI 서브커맨드로 구현한다(OQ-1 결정, 데몬 RabbitMQ-only 제약).
- A-2: P0(FR-1·2·7)와 P1(FR-3·4·5·8)은 별도 batch로 분리해 P0를 먼저 배포할 수 있다(CONFIRM 확인됨). FR-8 운영자 런북은 관측성 기능 사용법을 함께 문서화하므로 P1에 둔다(정확한 batch 분할은 suggest-batch-reqs/draft-batch에서 확정).
- A-3: PnL·수수료·세금 계산은 BAT-038(매도세+수수료) 정책을 단일 출처로 재사용한다.
- A-4: 배포 환경이 Loki 로그 수집을 사용한다(OQ-2 결정의 전제). 구조화(JSON) 출력은 Loki 미사용 환경에서도 stdout tail로 동작하므로 비파괴적이다.
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: FR-5 조회 표면을 운영자 CLI 서브커맨드 확장으로 할지, 신규 RabbitMQ RPC로 할지, 둘 다 제공할지. (HTTP는 제외)
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **운영자 CLI 서브커맨드(DB-backed)** 로 제공. RPC는 추가하지 않음. → FR-5 및 §5 범위에 반영.
- 종료 조건: 조회 표면 형태가 In Scope에 단일 값으로 고정되면 Answered. (충족)
- OQ-2: 기능 6(Loki 구조화 JSON 출력 + correlation id intent→order→fill 전파)을 이번 범위에 포함할지, 후속으로 Deferred할지.
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **이번 범위 In Scope.** 구조화(JSON) 출력 + correlation id 전파를 FR-7로 신설. FR-1·2·3·4 로그가 이 스트림으로 흐름. 배포 환경이 Loki를 사용한다는 전제(A-4)로 진행.
- 종료 조건: 범위 포함/Deferred가 확정되면 Answered. (충족)
- OQ-3: FR-4 거부 알림에서 ERROR 로그 외 "알림 전송 채널"(claude-bus/외부 webhook 등)을 이번 범위에 포함할지.
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **Deferred.** 이번 범위는 FR-4의 ERROR 로그까지만. 알림 전송 채널은 후속 반복/이슈로 인계(처리 방안: 차기 Discovery 입력으로 등록). → §5 Out of Scope에 반영.
- 종료 조건: 알림 전송 범위가 확정되면 Answered. (Deferred로 종료)
- OQ-4: FR-3 청산사유 분류를 신규 reason 값 추가(`watchdog_liquidation` 등)로 할지, 기존 reason 필드 + 보조 분류 필드로 할지. (계약 하위호환 방식)
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **Option A — 기존 reason 어휘를 계약으로 고정 + 기존 actor와 함께 노출. 신규 reason 값·신규 분류 필드 없음.** 근거: 코드 확인 결과 reason(조건축)·actor(주체축)가 이미 존재하며, 오해 원인은 둘이 합쳐 노출되지 않은 것. 우산 라벨(`watchdog_liquidation`)로 뭉치면 SL/TP/forced 구체 정보가 손실되어 비채택. → FR-3에 반영.
- 종료 조건: 어휘/매핑 방식이 확정돼 FR-3에 반영되면 Answered. (충족)
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 모든 로그 라인이 ISO8601(KST) 타임스탬프 + 레벨 prefix를 갖고, 주문 거부가 발생하면 ERROR 로그로 즉시 보인다(#28 유형 재현 시 로그만으로 인지 가능).
- S-2: 모든 주문 submit/결과(체결가·수량·수수료·세금 또는 거부+사유)가 단일 tailable 로그로 출력되어 `grep`으로 당일 체결/거부를 전수 추출할 수 있다.
- S-3: 청산 기록이 기존 reason 어휘 + actor를 함께 노출해, 안전망 자율청산(actor=safety_sell)과 전략 손절(actor=strategy)을 운영자가 구분할 수 있다(reason=stop_loss만으로 오해되지 않음).
- S-4: 운영자가 단일 CLI 호출로 보유·오늘 체결·실현 PnL(수수료·세금 반영)·대기 주문을 조회할 수 있고, 값이 `ledger_outbox` 수기 계산과 일치한다.
- S-5: `docs/operations.md`가 존재하고, 신규 운영자가 그 문서만으로 데몬 배포·실행, CLI로 상태/PnL 조회, 거부·미체결 디버깅을 수행할 수 있다(배포·CLI·로깅/관측성·트러블슈팅 4영역 모두 포함).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 단위 테스트(로그 포맷터)+라이브 데몬 로그 라인 검사 / 데이터 출처: 데몬 stdout 로그, `execution.py` 거부 경로.
- S-2 측정 방식: 거부/체결 시나리오 구동 후 로그 grep 검증(단위+end-to-end) / 데이터 출처: 데몬 로그, paper 브로커 체결.
- S-3 측정 방식: safety_sell 각 경로(stop_loss/take_profit/forced_close_at/market_close_liquidation) + 전략 매도 단위 테스트로 기록 reason·actor 확인 / 데이터 출처: `slot_transition_log`, 이벤트 payload.
- S-4 측정 방식: 체결 후 조회 CLI 결과와 `ledger_outbox` 직접 집계 비교 / 데이터 출처: `ledger_outbox`, 체결 기록, BAT-038 수수료·세금 정책.
- S-5 측정 방식: `docs/operations.md` 4영역(배포·CLI·로깅/관측성·트러블슈팅) 포함 여부 리뷰 체크 / 데이터 출처: `docs/operations.md`, `run-trex-core-daemon` SKILL, 신설 CLI.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1(조회 표면=CLI)·OQ-2(Loki/correlation In Scope)·OQ-3(알림 Deferred)·OQ-4(reason+actor) 결정으로 In/Out 경계의 플레이스홀더가 모두 해소됨 (충족)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4의 측정 방식과 데이터 출처가 테스트/로그/DB로 매핑됨 (충족)
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 현재 High 리스크 없음. Medium(R-2 로그포맷)·Low(R-1 reason 계약, R-3 PnL 집계)의 처리 방안이 명시됨 (충족)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개. OQ-1·2·4 Answered, OQ-3 Deferred 처리 완료 (충족)
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자(Justin Ko)가 confirm-discovery로 Freeze 확정 (confirm-discovery 단계에서 처리)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-029_20260616_operational-observability-logging-and-pnl-view.md
- 참조 문서:
- GitHub issue #29 (운영 관측성), issue #28 (호가단위 거부 — 관련 별도 트랙)
- docs/runtime-flows.md, docs/project-structure.md (baseline — 본 Discovery에서 갱신 대상)
- docs/interface-contract.md (조회 RPC/청산사유 어휘/거부 로그 계약 반영 대상)
- docs/operations.md (FR-8 신설 대상 운영자 런북), .claude/skills/run-trex-core-daemon/SKILL.md (런북 사실 자료 출처)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (해소됨) FR-5 조회 표면 = CLI(DB-backed), RPC 없음 — OQ-1 Answered
- (해소됨) Loki 구조화 출력 + correlation id 이번 범위 포함 — OQ-2 Answered (FR-7)
- (해소됨) 알림 전송 채널 = Deferred, ERROR 로그만 — OQ-3 Answered
- (해소됨) FR-3 청산사유 = 기존 reason 어휘 고정 + actor 함께 노출, 신규 필드 없음 — OQ-4 Answered (Option A)
## CONFIRM
- (확인됨, 2026-06-16, Justin Ko) P0(FR-1·2·7) batch 선행 → P1(FR-3·4·5·8) 후속 batch 분할 방향 동의. FR-8 운영자 런북은 P1(관측성 기능 사용법을 함께 문서화). 정확한 batch 경계는 suggest-batch-reqs에서 확정.
## DATA
- (해소됨) 배포 환경 Loki 사용 — A-4 전제로 진행 (OQ-2 결정에 반영)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: OQ-1~4 결정 완료 + FR-8(운영자 런북 docs/operations.md) 추가 반영. 신규 FR은 추가 OQ 없는 P1 문서 산출물. 본문·범위·성공기준(S-5) 정합. REQ 초안 작성 진행 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-16 12:20
@@ -1,229 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-030_20260616_slippage-limit-price-tick-size-snapping
- 이슈명: slippage 지정가 KRX 호가단위 미반영 → KIS 거부 (tick-size 스냅, issue #28)
- 작성 시각(KST): 2026-06-16 11:18
- 마지막 갱신 시각(KST): 2026-06-16 12:35
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-083 (신규, tick 유틸), REQ-070 (change-req CHG-20260616-01, 슬리피지 스냅)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub issue #28 (P0, 다음 거래일 전 수정 필수), REQ-070(slippage 지정가 — 호가단위 반올림을 "후속 주기"로 연기해 둔 당사자), 관련 issue #29/dcy-029(이 거부를 "관측"하는 별도 트랙)
- 해석:
- ORB v1 라이브 테스트(2026-06-16)에서 slippage 밴드가 적용된 매수 한계가가 KRX 호가단위를 위반해 KIS `주식주문호가단위 오류`로 전량 거부됐다. slippage_pct>0인 매수는 산출 한계가가 거의 항상 호가단위를 벗어나 0체결이 된다.
- REQ-070은 호가단위 정밀 반올림을 의도적으로 후속 주기로 미뤘고(int ceil/floor만 구현), 그 미뤄둔 항목이 이제 P0 정확성 버그로 표면화했다. 이번 Discovery는 KRX 호가단위 스냅을 구현하되, **반올림 방향을 REQ-070의 원래 메모(체결 보장 = 매수 상위/매도 하위)와 달리 "상한 준수(매수 내림/매도 올림)"로 전환**한다(승인자 결정).
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 유사
- 근거
- 기존 REQ-070(slippage 지정가 집행)의 동작 결함/미완 항목이다. REQ-070 Requirement/Notes가 "KRX 정밀 호가단위 반올림은 후속 주기"로 명시 연기했으므로, 새 기능이라기보다 REQ-070을 완성·정정하는 change-req 성격이다.
- 다만 반올림 방향 정책 전환과 가격대별 호가단위 테이블 도입이라는 결정 요소가 있어, change-req 근거를 남기기 위해 Discovery로 먼저 문제·정책을 정의한다(기존 REQ 본문 수정은 change-req로 수행).
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- `trex_core/application/execution.py:147-158`(`_dispatch`)이 slippage 밴드를 `매수=int(ceil(limit*(1+pct/100)))`, `매도=int(floor(limit*(1-pct/100)))`로 산출하고 **KRX 호가단위로 스냅하지 않는다**. 예: `ceil(147700*1.005)=148439` → 호가단위(100원) 위반 → KIS 거부.
- REQ-070(Status: Implemented) Requirement는 "KRX 호가단위 반올림은 체결 보장 방향(매수=상위, 매도=하위)으로 적용"을 적어뒀으나 Notes에서 "정밀 호가단위 테이블은 후속 주기, 현재는 int(ceil/floor)로 충분"으로 **미구현 상태로 남겨 둔 것**이 확인된다. 즉 현재 코드는 호가단위 스냅이 전혀 없다.
- 영향: `slippage_pct>0`인 매수는 산출 한계가가 거의 항상 호가단위를 벗어나 **전량 거부**(연결·시세 정상이어도 0체결). 매도(floor)도 limit_price+slippage를 쓰는 경로면 동일 위험. ORB v1처럼 slippage_pct를 쓰는 모든 인스턴스 해당.
- 매도 자율청산 경로: `trex_core/application/safety_sell.py`는 SL/TP/forced/market-close 모두 limit_price 없이 **시장가**(grep상 limit 미사용)라 이 버그 영향 밖. 영향받는 매도는 **전략 SellIntent(지정가+slippage)** 경로로 한정된다.
- baseline 문서(`docs/runtime-flows.md`, `docs/project-structure.md`)는 존재하며 최신. runtime-flows에 slippage dispatch 흐름은 기술돼 있으나 호가단위 스냅 단계는 없다.
- 기대 상태
- slippage 산출 한계가가 항상 **유효한 KRX 호가단위 배수**로 제출되어 `주식주문호가단위 오류` 거부가 사라진다.
- 반올림 방향은 **상한 준수**: 매수는 호가단위로 **내림**(설정 slippage 상한을 넘지 않음), 매도는 호가단위로 **올림**(매도 하한을 넘지 않음). 즉 `slippage_pct`를 하드 리스크 상한으로 취급한다.
- 가격대별 호가단위 산출이 순수 함수(가격→tick)로 분리되어 단위테스트로 경계값까지 검증된다.
- 이해관계자
- 운영 담당자(trex 운영팀): 매수 체결 복구가 1차 수요. 다음 거래일 전 수정 필요.
- 전략팀(trex-orb-v1 등 slippage_pct 사용 인스턴스): 거부 없는 체결의 직접 수혜자.
- 문서 작성자(AI agent) / 승인자(Justin Ko): 반올림 방향·범위 확정.
- 제약사항
- KRX 주식 호가단위(2023.1.25~)는 가격 구간별로 다르다(아래 표). ETF/ETN 등은 호가체계가 다를 수 있다.
- `slippage_pct`는 리스크 상한이므로 스냅으로 상한을 넘기면 안 된다(반올림 방향 결정의 근거).
- 변경은 dispatch-time 변환만 — 슬롯의 `limit_price`(=entry_price) 원본은 덮어쓰지 않는다(REQ-070 설계 보존).
KRX 주식 호가단위 (2023.1.25~):
| 가격 구간(원) | 호가단위(원) |
| --- | --- |
| ~ 2,000 | 1 |
| 2,000 ~ 5,000 | 5 |
| 5,000 ~ 20,000 | 10 |
| 20,000 ~ 50,000 | 50 |
| 50,000 ~ 200,000 | 100 |
| 200,000 ~ 500,000 | 500 |
| 500,000 ~ | 1,000 |
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 전략이 entry_price=147,700, slippage_pct=0.5%로 매수 인텐트를 내면, dispatch가 산출 한계가 148,439를 **148,400(호가단위 내림)** 으로 스냅해 유효 지정가로 제출하고 정상 체결된다(거부 없음).
- 전략이 entry_price=54,500, slippage_pct=0.5%로 매수하면 54,772.5 → **54,700(내림)** 으로 제출되어 0.5% 상한 안에서 체결된다.
- 전략이 지정가+slippage로 매도하면 산출 한계가가 **호가단위 올림**으로 스냅되어 매도 하한을 넘지 않는 유효 지정가로 제출된다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- KRX 호가단위 스냅 도입: slippage 산출 한계가를 가격대별 호가단위 유효 배수로 정규화. 방향 = **상한 준수(매수 내림 / 매도 올림)**.
- 호가단위 산출 유틸리티 신설: 가격 → tick 순수 함수(KRX 2023.1.25~ 구간표). 재사용·테스트 가능.
- REQ-070 정정: 호가단위 반올림 방향을 원래 메모의 "체결 보장(매수 상위/매도 하위)"에서 "**상한 준수(매수 하위/매도 상위)**"로 전환하고, "후속 주기"로 미뤄둔 정밀 호가단위 테이블을 이번에 구현으로 확정(change-req).
- baseline 문서 갱신: `docs/runtime-flows.md`의 slippage dispatch 흐름에 호가단위 스냅 단계 추가, 신규 tick 유틸 모듈이 생기면 `docs/project-structure.md`에 반영.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/execution.py`(`_dispatch`): slippage 산출 직후 호가단위 스냅 적용(매수 내림/매도 올림).
- 신규: 호가단위 유틸(가격→tick, tick 배수 내림/올림). 위치는 design에서 확정(예: `trex_core/domain/` 또는 `trex_core/application/` 순수 함수 모듈).
- 문서 영향:
- `docs/runtime-flows.md`(slippage dispatch + 호가단위 스냅 흐름), `docs/project-structure.md`(신규 tick 유틸 모듈 책임), REQ-070 본문/AC(change-req).
- 운영 영향:
- slippage_pct 사용 인스턴스의 매수 거부가 해소돼 체결 복구. 반올림 방향 변경으로 체결가가 entry에 더 가깝게(매수) 형성됨(상한 미초과). 배포 절차 변경 없음.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 가격 → KRX 호가단위(tick)를 산출하는 순수 함수를 제공한다(2023.1.25~ 구간표). 구간 경계값에서 정확한 tick을 반환한다.
- FR-2: slippage 산출 한계가를 호가단위 유효 배수로 스냅한다. 방향 = **상한 준수**: 매수 = 해당 tick의 **내림 배수**(≤ 산출 한계가, slippage 상한 미초과), 매도 = 해당 tick의 **올림 배수**(≥ 산출 한계가, 매도 하한 미초과).
- FR-2b: band < 1 tick 엣지에서 미체결을 막기 위해 **하한(매수)/상한(매도)을 보장**한다(OQ-2 결정). 매수 실효가 = `max(floor_to_tick(entry×(1+pct)), ceil_to_tick(entry))`, 매도 실효가 = `min(ceil_to_tick(entry×(1-pct)), floor_to_tick(entry))`. 즉 slippage가 1틱 이상이면 정상 반영, 1틱 미만이어도 최소 entry의 유효 호가는 보장한다. 이 보장값도 slippage 상한/하한을 넘지 않는다.
- FR-3: `limit_price=None`(시장가) 또는 `slippage_pct` 없음/0인 경로는 기존 동작을 보존한다(호가단위 스냅·회귀 없음). slippage가 적용된 경우에만 스냅한다.
- FR-4: REQ-070을 change-req로 정정한다 — 반올림 방향을 "상한 준수(매수 하위/매도 상위)"로 변경하고, 정밀 호가단위 테이블 구현을 확정한다. 변경 근거(상한 준수 > 체결 보장)를 Change Log에 남긴다.
- FR-5: 이 변경이 런타임 흐름/구조를 바꾸므로 `docs/runtime-flows.md`(slippage dispatch 호가단위 스냅)와 필요 시 `docs/project-structure.md`(신규 tick 유틸 모듈)를 갱신한다.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1 (정확성): 호가단위 스냅 후 제출 지정가는 항상 유효한 KRX 호가단위 배수이며, `slippage_pct`가 정의한 가격 상한(매수)/하한(매도)을 넘지 않는다.
- NFR-2 (하위호환): slippage 미사용·시장가 경로의 기존 동작과 REQ-070 AC-3~6의 회귀 없음을 보장하는 additive 변경이다.
- NFR-3 (테스트 가능성): 가격→tick 함수와 스냅 방향을 각 가격 구간 경계값(2,000 / 5,000 / 20,000 / 50,000 / 200,000 / 500,000)에서 단위테스트로 검증한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- FR-1·FR-2·FR-2b·FR-3·FR-4·FR-5. KRX **주식** 호가단위 스냅(상한 준수 방향 + band<tick 하한/상한 보장), tick 유틸, REQ-070 change-req, baseline 문서 갱신.
- Out of Scope
- issue #29/dcy-029(거부 관측성 — 별도 트랙).
- ETF/ETN 등 비주식 호가체계 지원 — 이번 반복 제외, **주식 전용**(OQ-1 결정). 근거: universe 항목(`PortfolioItem`)에 종목유형 필드가 없어 분기 불가하며, ETF 지원은 portfolio-hub 계약 변경을 수반. 주식 tick은 2,000원 이상 구간에서 모두 5의 배수라 ETF에도 유효 주문이 나옴. **알려진 한계: 2,000원 미만 ETF는 주식 tick(1원) 스냅 시 무효 가능 → 미지원**. ETF/ETN은 실제 편입 + 종목유형 필드 도입과 함께 후속 Discovery로 인계.
- slippage_pct 정책 자체(밴드 폭·기본값) 변경 — 본 Discovery는 호가단위 스냅과 반올림 방향만 다룬다.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Medium): 반올림 방향 전환(매수 내림)이 slippage 헤드룸을 한 틱 줄여 체결확률이 미세하게 낮아질 수 있다(특히 변동성 구간).
- 완화 방안: 매수 내림은 entry 기준 여전히 +slippage 범위 안의 상위 가격이라 헤드룸 대부분 유지. 거부(0체결) 해소 효과가 훨씬 크다. 필요 시 운영 데이터로 체결률 모니터링.
- R-2 (Low): slippage_pct가 한 틱보다 작은 경우(band < 1 tick) 매수 내림 스냅이 entry 자체 호가 또는 그 아래로 떨어져 미체결될 수 있다. (OQ-2=하한 보장 결정으로 완화 → Low)
- 완화 방안: FR-2b 하한/상한 보장 — 매수는 `ceil_to_tick(entry)`를 하한으로, 매도는 `floor_to_tick(entry)`를 상한으로 보장해 최소 entry 유효 호가에서 체결 가능. slippage 상한도 미초과.
- R-3 (Low): 가격대별 호가단위 표가 KRX 제도 변경 시 낡을 수 있다(2023.1.25 기준).
- 완화 방안: tick 표를 단일 유틸에 상수로 모아 변경 지점을 1곳으로, 출처·기준일을 주석/문서에 명시.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 현재 universe는 KRX 주식 중심이고 종목유형 필드가 없어, 이번 반복은 주식 호가단위 표로 한정한다(OQ-1 결정).
- A-2: slippage가 적용되는 매도는 전략 SellIntent(지정가) 경로뿐이고, safety-sell 자율청산은 시장가라 영향 밖이다(코드 확인).
- A-3: 변경은 dispatch-time 변환만 — 슬롯 `limit_price` 원본은 보존한다(REQ-070 설계).
## 오픈 질문
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
- OQ-1: ETF/ETN 등 비주식 호가체계 종목을 이번 범위에 포함할지(별도 tick 표 필요), 주식 전용으로 한정할지.
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **주식 전용.** 근거: universe `PortfolioItem`에 종목유형 필드 부재(코드 확인, `hub_read.py:114-120`) → 분기 불가. ETF 지원은 portfolio-hub 계약 변경 수반. 주식 tick은 2,000원 이상에서 5의 배수라 ETF에도 유효. 2,000원 미만 ETF는 미지원(known limitation). ETF/ETN은 후속 Discovery로 인계. → §5 Out of Scope에 반영.
- 종료 조건: 범위가 In/Out으로 고정되면 Answered. (충족)
- OQ-2: band < 1 tick 엣지(매수 내림 스냅이 entry 호가 이하로 떨어지는 경우)의 처리 정책.
- 상태: Answered (2026-06-16, Justin Ko)
- 결정: **하한/상한 보장.** 매수 = `max(floor_to_tick(entry×(1+pct)), ceil_to_tick(entry))`, 매도 = `min(ceil_to_tick(entry×(1-pct)), floor_to_tick(entry))`. slippage<1tick이어도 최소 entry 유효 호가 보장(미체결 방지), slippage 상한 미초과. → FR-2b로 반영.
- 종료 조건: 엣지 동작이 확정돼 FR-2(b)에 반영되면 Answered. (충족)
- OQ-3: issue #28 미해결 의문 — 2026-06-15 064400이 100,300에 정상 체결된 건이 slippage 미적용(시장가)이었는지.
- 상태: Deferred
- 처리 방안: delivery/verification 단계에서 해당 intent payload(slippage_pct 유무) 확인으로 검증. 본 Discovery confirm을 막지 않는 운영 데이터 검증 항목으로 인계.
- 종료 조건: intent payload로 시장가/지정가 여부가 확인되면 Answered.
### 오픈 질문 처리 기준
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: issue #28의 두 케이스(147,700 / 54,500 @ 0.5% 매수)가 각각 **148,400 / 54,700**(호가단위 내림, 유효 호가, 상한 미초과)으로 산출된다.
- S-2: slippage 적용 매수/매도 산출 한계가가 모든 가격 구간에서 항상 유효한 KRX 호가단위 배수이고, 매수 ≤ 산출값 / 매도 ≥ 산출값(상한·하한 준수)이다.
- S-3: slippage 미사용·시장가·slippage_pct=0 경로가 기존 동작과 동일하다(REQ-070 AC-3~6 회귀 없음).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: 단위 테스트(두 케이스 명시 검증) / 데이터 출처: issue #28 케이스, `execution.py` `_dispatch`.
- S-2 측정 방식: 각 가격 구간 경계값 파라미터라이즈드 단위 테스트(tick 함수 + 스냅 방향) / 데이터 출처: KRX 호가단위 표, tick 유틸.
- S-3 측정 방식: REQ-070 기존 AC 회귀 테스트 통과 / 데이터 출처: `tests/application/test_execution.py`, REQ-070 AC-3~6.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: OQ-1(주식 전용)·OQ-2(하한 보장) 결정으로 In/Out 경계 플레이스홀더가 해소됨 (충족)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3의 측정 방식이 테스트/AC로 매핑됨 (충족)
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 현재 High 리스크 없음. Medium(R-1 체결확률)·Low(R-2 band<tick, R-3 tick표 노후)의 처리 방안 명시 (충족)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1·2 Answered, OQ-3 Deferred. `Open` 0개 (충족)
- [ ] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 승인자(Justin Ko)가 confirm-discovery로 Freeze 확정 (confirm-discovery 단계에서 처리)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-030_20260616_slippage-limit-price-tick-size-snapping.md
- 참조 문서:
- GitHub issue #28 (호가단위 거부), issue #29/dcy-029 (관측성 — 관련 별도 트랙)
- docs/srs/Integration/req-070_slippage-limit-price-execution.md (정정 대상 REQ)
- docs/runtime-flows.md, docs/project-structure.md (baseline — 본 Discovery에서 갱신 대상)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE
- (해소됨) 반올림 방향 = 상한 준수(매수 내림/매도 올림) — 승인자 결정, FR-2/§4에 반영
- (해소됨) 호가단위 범위 = 주식 전용, ETF/ETN Deferred — OQ-1 Answered
- (해소됨) band<1tick 엣지 = 하한/상한 보장(FR-2b) — OQ-2 Answered
## CONFIRM
- (확인됨, 2026-06-16, Justin Ko) REQ-070 반올림 방향을 원래 메모(체결 보장=매수 상위/매도 하위)에서 상한 준수(매수 하위/매도 상위)로 전환하는 change-req 방향 동의. 반올림 방향 (A) 결정에 내포됨.
## DATA
- (해소됨) universe 종목유형 필드 부재 확인(`hub_read.py`) → 주식 전제 — OQ-1 결정에 반영
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: 반올림 방향(상한 준수), OQ-1(주식 전용), OQ-2(하한 보장) 모두 결정 완료. REQ-070 change-req 방향 동의. 저장소 사실관계(execution.py 미스냅, REQ-070 연기, hub_read 유형필드 부재) 확인. REQ 초안 작성 진행 가능.
- Ready for REQ Drafting: true
- Confirmed By: Justin Ko
- Confirmed At (KST): 2026-06-16 11:30
@@ -1,182 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-031_20260619_event-publisher-connection-reliability
- 이슈명: 이벤트 발행 연결 신뢰성 — idle heartbeat 사망 후 첫 발행 유실 + DLQ 가시성 (OPS-005)
- 작성 시각(KST): 2026-06-19 23:00
- 마지막 갱신 시각(KST): 2026-06-19 23:50
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-091, REQ-092, REQ-093, REQ-094
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: ROADMAP.md(M2 트랙B), memory/trex-backlog.md(Task #16)
- 해석:
- 운영팀 OPS-005 리포트(trex-core RabbitMQ 발행 실패)를 코드로 검증한 결과, 발행 전용 연결의 idle heartbeat 사망 후 첫 발행이 재시도·DLQ 없이 유실되는 실결함을 확인했다.
- M2(매도전략 tsl 라이브, 6/28 목표)에서 슬롯 전이 이벤트 유실은 매도 미발동=실손실로 직결되므로, tsl 라이브 선행 조건으로 발행 신뢰성을 보강한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 dcy-006(RabbitMQ transport 도입)·dcy-009(멀티 인스턴스 토폴로지)는 전송 계약 수립이 주제였고, 발행 연결의 idle 수명/재시도 신뢰성은 다루지 않았다.
- OPS-005는 운영 환류로 발견된 신규 신뢰성 결함이며 독립 주기로 추적하는 것이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다.
- 현재 상태
- 발행자는 단일 공유 인스턴스다(`runtime/daemon.py:206` `self.event_publisher = _build_transport`). 슬롯 전이 이벤트는 `_make_event_emitter`(`daemon.py:238-248`)가 `publish_event`로 topic exchange `trex.events`에 발행한다.
- 발행 전용 연결은 publish 사이에 `process_data_events`를 돌리지 않는다(consumer 루프와 달리 — `rabbitmq.py:337/379`는 0.2초마다 호출). `params.heartbeat = 30`(`rabbitmq.py:83`)이나 idle 구간엔 heartbeat가 나가지 않아 ~60초 후 RabbitMQ 서버가 연결을 종료한다.
- 다음 발행 시 `_ensure_connected`(`rabbitmq.py:94-103`)가 stale `is_open=True`를 신뢰해 재연결을 건너뛰고, `basic_publish`(confirm_delivery 대기, `rabbitmq.py:88/180`)가 실패한다 → `AMQPError`(`rabbitmq.py:193`) → `_mark_disconnected` + `TransportUnavailableError`.
- 이 예외가 `emit()`(`daemon.py:245`)로 올라가 `:246`에서 잡혀 warning 로그만 남기고 삼켜진다("publish failure must not roll back"). 재시도·DLQ 없이 **그 이벤트는 유실**된다. 다음 발행은 재연결돼 정상 → 증상 = 한산 구간 직후 첫 이벤트 간헐 유실.
- 한편 unroutable(구독자 없음)은 `mandatory=True` → `UnroutableError`(`rabbitmq.py:190`) → `_publish_dlq`로 `trex.events.dlq`(durable)에 보존된다 → 유실 아님. 단 DLQ를 소비/알림하는 주체가 없어 조용히 누적되는 가시성 공백이 있고, `_publish_dlq`는 best-effort(`rabbitmq.py:215` 실패 시 로그만)다.
- 기대 상태
- 발행 연결의 idle 사망을 예방하고, 사망/half-open 시에도 첫 발행이 투명 재시도로 복구되어 이벤트가 유실되지 않는다.
- DLQ(unroutable + 발행 실패 양쪽)의 유입/깊이가 운영에서 관측·경보 가능하다.
- 이해관계자
- 작성/검수: 팀장(개발 거버넌스)
- 구현: trex-core-dev
- 영향 소비자: trex-orb-v1(라이브), trex-tsl-v1(M2 라이브 예정)
- 운영: TREX 운영팀(DLQ 가시성/경보 소비)
- 제약사항
- trex-core는 M1 운영 배포본(`9e35467`)이다. wire 계약·기존 consume/RPC 경로 무회귀가 필수다.
- pika BlockingConnection은 스레드 비안전 — keepalive는 발행 전용 연결을 소유한 스레드에서만 I/O를 돌려야 한다.
## 주요 사용자 시나리오
- 점심·저활동·개장 직후 등 한산한 구간 뒤 슬롯이 HOLDING으로 전이될 때, 첫 전이 이벤트가 유실되지 않고 tsl가 수신해 트레일링을 시작한다.
- RabbitMQ 일시 단절/재시작 후에도 발행이 재연결·재시도로 복구되어 청산 전이 이벤트가 누락되지 않는다.
- 운영자가 DLQ 유입(구독자 없음 또는 발행 실패)을 대시보드/로그/경보로 인지하고 환류한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다.
- (B) 발행 재시도 + 강제 재연결: `publish_event`가 `is_open`을 맹신하지 않고, 발행 실패 시 강제 재연결 후 1회 재시도한다. 재시도까지 실패한 경우에만 실패로 처리한다.
- (A) idle keepalive: 발행 전용 연결에 heartbeat 주기 내 `process_data_events` 틱을 보장해 서버 측 연결 종료를 예방한다.
- (D) DLQ 가시성/경보: `trex.events.dlq` 유입·깊이를 관측 가능하게 한다(unroutable + 발행 실패 양쪽 추적). 운영 관측성(dcy-029) 자산 재사용 여부 검토.
## 영향 범위
- 코드 영향:
- `trex_core/adapters/messaging/rabbitmq.py`(`publish_event`/`_ensure_connected`/`_publish_dlq` + keepalive 추가)
- `trex_core/runtime/daemon.py`(`_make_event_emitter` 실패 처리 강화, keepalive 틱 연동)
- 문서 영향:
- `docs/operations.md`(DLQ 가시성/경보 운영 절차), `docs/runtime-flows.md`(발행 재시도/keepalive 흐름) 갱신 검토
- 운영 영향:
- DLQ 모니터링/경보 신규 절차. 배포는 trex-core 재배포 필요(M1 배포본 갱신) — 사장님 release 결재 대상.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1 (B): `publish_event`는 연결 종료/half-open으로 발행 실패 시 강제 재연결 후 1회 재시도한다. 재시도까지 실패한 경우에만 `TransportUnavailableError`를 올린다. `is_open` 기반 스킵에 의존하지 않는다.
- FR-2 (A): 발행 전용 연결에 idle keepalive를 도입해 heartbeat 주기 내 I/O가 보장되도록 한다(서버 측 idle 종료 예방). pika 스레드 안전성을 위반하지 않는다.
- FR-3 (D): `trex.events.dlq` 유입/깊이를 운영에서 관측·경보 가능하게 한다(메트릭/로그/소비자 중 택1). unroutable과 발행 실패 양쪽이 추적되어야 한다.
- FR-4: `_make_event_emitter`의 발행 실패 처리를 보강한다 — 재시도 후에도 실패 시 단순 warning 삼킴이 아니라 식별 가능한 경보 레벨 로그/메트릭을 남긴다.
## 비기능 요구사항
- NFR-1: M1 운영 배포본(`9e35467`) 무회귀 — 기존 consume/RPC/wire 계약과 토폴로지(`declare_topology`)는 불변.
- NFR-2: 재시도/keepalive가 execution-loop를 장시간 블록하지 않는다(매도 타이밍 보호) — 재시도 1회 + 명시적 타임아웃 상한.
- NFR-3: 변경은 transport 어댑터·발행 경로에 국소화하고, 슬롯 도메인/broker 집행 로직은 건드리지 않는다.
## 범위 경계
- In Scope
- 발행 전용 연결의 재시도·재연결·keepalive(B+A)
- DLQ 유입/깊이 가시성·경보(D)
- emit 실패 처리 강화(FR-4)
- Out of Scope
- 이벤트 영속 outbox(옵션 C) — 본 주기 제외, 필요 시 후속(M3) Discovery. (사장님 스코프 결정 B+A+D)
- consumer(orb/tsl) 측 idempotency 구현 — 본 주기는 검증/확인까지, 구현은 해당 repo 별도 추적
- RPC client(`call_rpc`) 연결 정책 — 본 주기는 이벤트 발행 경로 한정
# 6. 리스크/가정 목록
## 리스크
- R-1 (High): trex-core는 M1 라이브 배포본이고 발행 경로는 orb(라이브)·tsl(M2)에 동시 영향 → 회귀 시 사고 반경 큼.
- 완화 방안: opus QC 강제, 기존 회귀 스위트 + wire 계약 무회귀 검증, transport 어댑터로 변경 국소화.
- R-2 (Medium): keepalive가 pika 스레드 비안전성과 충돌 가능(다른 스레드에서 I/O 호출 시 손상).
- 완화 방안: 발행 전용 연결을 소유한 스레드에서만 keepalive I/O 실행하도록 설계 단계에서 확정(OQ-1).
- R-3 (Medium): 재시도는 at-least-once 의미 → 동일 이벤트 중복 발행 가능.
- 완화 방안: 소비자(orb/tsl) idempotency 검증(OQ-2). 슬롯 전이 이벤트는 멱등 적용 가능 추정.
## 가정
- A-1: 본 변경은 transport 어댑터·발행 경로에 한정되며 trex-core dcy-028(strategy-state, BAT-042 released) 등 기존 계약은 불변이다.
- A-2: 슬롯 전이 이벤트 소비자(orb/tsl)는 중복 수신에 견고하거나 견고화 가능하다.
- A-3: DLQ 가시성은 dcy-029(운영 관측성) 로깅/뷰 자산을 일부 재사용할 수 있다.
## 오픈 질문
- OQ-1: keepalive를 별도 스레드로 둘지, execution-loop 틱에 발행 연결 `process_data_events`를 삽입할지 (pika 스레드 안전성 제약).
- 상태: Deferred
- 처리 방안: draft-batch-design에서 trex-core-dev가 스레드 소유 모델 기준으로 결정. REQ에 설계 결정 추적 인계.
- 종료 조건: batch design 문서에 keepalive 실행 위치·스레드 소유가 확정되면 Answered 처리.
- OQ-2: 재시도 at-least-once로 인한 이벤트 중복 시 소비자(orb/tsl)가 멱등한가.
- 상태: Deferred
- 처리 방안: delivery 단계에서 trex-orb-v1/trex-tsl-v1 이벤트 핸들러의 멱등성 확인(§10 DATA로 추적). 비멱등이면 해당 repo 후속 티켓. confirm-discovery를 막지 않음(전이 이벤트는 멱등 적용 가능 추정).
- 종료 조건: 두 소비자의 중복 수신 동작이 확인되면 Answered.
- OQ-3: DLQ 가시성을 메트릭/로그/소비자 데몬 중 무엇으로 구현할지.
- 상태: Deferred
- 처리 방안: draft-batch-design에서 dcy-029 관측성 자산 재사용 가능성과 함께 결정.
- 종료 조건: batch design에 DLQ 가시성 구현 방식이 확정되면 Answered.
# 7. 초기 성공 기준
- S-1: idle(>heartbeat×2) 구간 직후 발행이 유실 없이 성공한다(재연결·재시도로 복구).
- S-2: RabbitMQ 강제 단절/재시작 시나리오에서 발행 이벤트 누락 0건.
- S-3: DLQ 유입(unroutable + 발행 실패)이 관측 지표로 노출되고 경보 가능하다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: idle 주입 후 발행 통합 테스트(연결 강제 종료 후 publish) / 데이터 출처: trex-core 테스트 스위트(신규 케이스).
- S-2 측정 방식: broker 재시작 fault-injection 테스트에서 발행 카운트 vs 수신 카운트 대조 / 데이터 출처: 통합 테스트 + trex-sim e2e(M2 트랙C 연계).
- S-3 측정 방식: DLQ 깊이/유입 메트릭 노출 확인 / 데이터 출처: 관측 지표(로그/메트릭), `docs/operations.md`.
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 사장님 스코프 B+A+D 확정 반영 + outbox(C) 제외 합의 명시 (2026-06-19 확정)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~3의 테스트/메트릭 출처가 trex-core 스위트·trex-sim e2e로 연결됨
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(회귀) 해소 책임 = trex-core-dev 구현 + opus qc-reviewer (CONFIRM① 확정)
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1/3 Deferred(design 인계), OQ-2 Deferred(delivery 검증) 처리 완료 — Open 0개
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 사장님(CEO) via 팀장
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-031_20260619_event-publisher-connection-reliability.md
- 참조 문서:
- trex_core/adapters/messaging/rabbitmq.py
- trex_core/runtime/daemon.py
- docs/discovery/dcy-006_20260605_strategy-module-rabbitmq-transport.md
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- (해소됨) 수정 스코프 = B(재시도)+A(keepalive)+D(DLQ 가시성). outbox(C)는 제외→M3 후보. — 사장님 결정 2026-06-19, §5에 반영.
## CONFIRM
- (확정) opus QC: 본 batch는 고위험(M1 라이브 trex-core)이라 qc-reviewer를 opus로 승격 운용. — 사장님 확정 2026-06-19.
- (확정) M2 게이트: tsl(M2) 라이브 전 선행 완료 — 6/28 릴리즈 창 내 배포·검증. — 사장님 확정 2026-06-19.
## DATA
- {{DATA_CONSUMER_IDEMPOTENCY}}: orb/tsl 이벤트 핸들러의 중복 수신 멱등성 확인 결과(OQ-2 종료용).
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: confirm-discovery 완료. 스코프 B+A+D 확정, CONFIRM 2건(opus QC·M2 게이트) 확정, OQ 전부 Deferred 처리(Open 0). DATA 1건(소비자 멱등성)은 delivery 검증으로 추적. trex-core-dev에 draft-req 인계.
- Ready for REQ Drafting: true
- Confirmed By: 사장님(CEO) via 팀장
- Confirmed At (KST): 2026-06-19 23:20
@@ -1,202 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-032_20260621_fill-missing-price-cost-basis-backfill
- 이슈명: 체결가 누락(브로커 평균가 0/None) 시 슬롯 평단·원장 cost basis 0 오염(무경고·영구) — 지정가 백필 + 로깅 + 알람 검토
- 작성 시각(KST): 2026-06-21 (KST)
- 마지막 갱신 시각(KST): 2026-06-21 (KST)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-095, REQ-096, REQ-097
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: 팀장 워크스페이스 백로그 `trex-backlog.md`("avg_price=None → 무경고 고정 2% 폴백" finding에서 상류 원인으로 추적)
- 해석:
- tsl tier 폴백(avg_price=None) finding을 상류로 추적한 결과, 진짜 원인은 trex-core 체결 처리에서 브로커 평균체결가가 0/None으로 올 때 **슬롯 평단과 원장 cost basis가 동시에 0으로, 무경고·영구히 오염**되는 데이터 무결성 갭이었다.
- 사장님 방향은 "지정가(주문 limit_price) 백필 + 로깅 + (이 기회에) 알람 기능 검토"이며 본 건은 M2 포함으로 확정되었다. 본 Discovery는 그 방향을 trex-core 변경 단위로 정식화한다.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-026(매도 세금/수수료 정책), dcy-029(운영 관측성·PnL 뷰)는 체결 *기록*과 인접하나, "체결가 자체가 0/None일 때의 무결성 방어"는 어디서도 다루지 않았다. 기존 코드의 `if avg else 0.0`/`fs.avg_price if not None else 0.0`은 방어가 아니라 0 통과 경로다.
- 신규 알람(alert) 능력 검토가 포함되어, 기존 Discovery의 미세 조정이 아니라 독립 주기로 추적하는 편이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다.
- 현재 상태
- KIS 체결조회(`inquire-daily-ccld`)가 체결수량(`ccld_qty`)은 보고하나 평균체결가(`avg`)가 없거나 0으로 오는 순간 스냅샷이 있을 수 있다. `adapters/broker/kis.py:209,219`(완전체결)·`:232,238`(부분체결+취소)·`:243`(부분체결)에서 `fill_price = float(avg) if avg else 0.0` → `avg_price = fill_price or None`로, **체결인데 가격이 None/0**인 FillStatus가 생성된다.
- 이 값이 두 곳을 동시에 오염시킨다. (1) 슬롯 평단: 초기 매수 시 `application/execution.py:242-251`에서 `old_avg=0, fill_price=0 → new_avg=0`로 HOLDING 전이 → `slot.avg_price=0` 저장. (2) 원장: `application/execution.py:424` `price = float(fs.avg_price) if fs.avg_price is not None else 0.0` → **매수가 price=0으로 ledger_outbox에 기록**.
- **무경고**: 원장 skip 가드는 `portfolio_id` 부재(`:405`)·`execution_id` 부재(`:415`) 두 가지뿐이고 둘 다 WARN한다. "체결됐는데 price≤0"은 어느 가드에도 안 걸려 **조용히 통과**한다.
- **영구성**: 완전체결(`ccld_qty >= ord_qty`)이면 즉시 HOLDING 전이 + `clear_dispatch`(`execution.py:248-285`)라 **다음 폴링을 하지 않는다.** KIS가 다음 틱에 올바른 평균가를 줘도 다시 읽지 않아 0이 영구히 박힌다.
- **하류 영향 확인됨**: 슬롯 평단 0 → slots RPC가 0.0 전송(`messaging_handlers.py:561`) → tsl이 `≤0 → None`(tsl_strategy.py:213)로 보고 tier 무시 2% 폴백 매도(무탐지). 원장 price=0 → 실현손익이 cost basis가 아닌 당일 net 현금흐름으로 계산되므로(`operator_report.py`) 매수 대금 과소 → **실현손익 과대 표시**.
- trex-core에는 구조화된 알람(alert) 채널이 없다. 운영자 표면은 CLI `trex-core report` + 구조화 로그가 전부이며, 이상 징후를 푸시로 알리는 메커니즘은 부재(`daemon.py:248`에 alert 주석 1건뿐).
- 기대 상태
- 체결됐는데 브로커 평균체결가가 없거나 0이면, 슬롯 평단과 원장 price를 **주문 지정가(limit_price)로 백필**해 cost basis가 0으로 오염되지 않는다.
- 백필이 발생하면 반드시 식별 가능한 WARN 로그(슬롯·심볼·주문·대체가)를 남겨 사후 탐지·정합화가 가능하다.
- (검토 대상) 이런 무결성 이상을 운영자에게 즉시 알리는 알람 채널의 도입 가치/범위를 평가한다.
- 이해관계자
- 팀장(Claude) — Discovery/게이트 관리, 본 finding 추적자
- 사장님 — M2 범위·알람 채널·비가역 운영 결정 승인자
- trex-core-dev — delivery 담당
- 운영자 — 백필/이상 알림의 최종 소비자, 첫 거래일 매도 모니터링 주체
- 제약사항
- trex-core wire 계약(slots RPC 필드 등) 변경은 blast radius가 커 본 건 범위에서 가급적 피한다(slot.avg_price 값 정정은 wire 스키마 변경이 아님).
- 백필 가격은 추정치다 — 실제 체결가와 다를 수 있으므로 "정확값"이 아니라 "0보다 합리적인 근사 + 추적 가능"이 목표다.
- orb 매수는 지정가 주문이라 limit_price가 존재한다는 전제에 의존한다(시장가/지정가 부재 케이스는 별도 폴백 필요).
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자: 장중 매수 체결 직후 KIS 체결조회가 일시적으로 평균가 빈 행을 반환 → 현재는 슬롯 평단 0 + 원장 매수 price 0이 영구히 남아 그날 실현손익 리포트가 부풀려진다. 백필 후엔 지정가 근사로 기록되고 WARN으로 추적된다.
- tsl 매도 데몬: 평단 0인 holding 슬롯을 받아 tier 무시 2% 폴백으로 매도(정책 위반)하고 아무도 모른다. 백필 후엔 정상 tier로 평가된다.
- 운영자(알람 검토 시나리오): 무결성 이상(백필 발생, 0가격 체결)이 즉시 푸시 알람으로 도달해 당일 수기 점검 없이도 인지한다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다.
- 변경 1 — 체결가 무결성 가드 + 지정가 백필: 체결(완전/부분) 처리에서 브로커 평균체결가가 None/0이면, 슬롯 평단과 원장 price를 주문 지정가(`slot.limit_price`/주문 컨텍스트)로 백필한다. 지정가도 없으면 최종 폴백 정책을 적용한다(범위 경계 참조).
- 변경 2 — 백필/0가격 체결 로깅: 백필 또는 price≤0 체결 통과 시 식별 가능한 WARN 로그를 남긴다(슬롯·심볼·주문·원가격·대체가격·사유). 원장 skip 가드와 동일한 운영 가시성 수준으로 맞춘다.
- 변경 3 — 알람(alert) 능력 검토(스파이크): trex-core 무결성 이상(백필 발생 등)을 운영자에게 푸시로 알리는 알람 채널의 도입 가치·범위·채널을 평가하고, M2 포함 여부와 최소 구현 범위를 결정한다.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시한다.
- 코드 영향:
- `trex_core/application/execution.py` — `_on_filled`/`_on_partial_cancel`의 평단 계산(`:242-251`,`:305-314`)과 `_record_ledger`의 price 결정(`:424`)에 백필 분기 추가.
- `trex_core/adapters/broker/kis.py` — 평균가 None/0 표면화 지점(`:209,219,232,238,243`)의 의미 정합성 검토(어디서 백필할지 결정: adapter vs application).
- (알람 채택 시) 신규 alert 포트/어댑터 모듈 + 호출 지점. 본 Discovery에서는 검토만, 구현 범위는 OQ로 둔다.
- 문서 영향:
- `docs/discovery/index.md` 신규 행 추가.
- (알람 채택 시) `docs/runtime-flows.md`에 알람 흐름 추가 가능 — 채택 결정 후 반영.
- 운영 영향:
- 백필 발생 시 원장에 지정가 근사가 들어가므로, 운영자 PnL 리포트의 정확도가 개선되되 "근사"임을 런북에 명시 필요.
- (알람 채택 시) 운영자 알림 수신 채널·임계 설정이 신규 운영 절차로 추가됨.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 체결(완전·부분) 처리에서 브로커 평균체결가가 None 또는 ≤0이면, 슬롯 평단(`slot.avg_price`)을 주문 지정가로 백필해 0으로 저장되지 않게 한다.
- FR-2: 동일 조건에서 원장 기록 price(`_record_ledger`)도 동일한 백필 가격을 사용해 매수/매도가 price=0으로 기록되지 않게 한다.
- FR-3: 백필이 발생하거나 price≤0 체결이 통과할 때, 슬롯·심볼·주문ID·원가격·대체가격·사유를 포함한 WARN 로그를 남긴다(원장 skip 가드와 동일한 가시성 수준).
- FR-4: 알람(alert) 능력을 검토해 (a) 도입 여부, (b) 채널(로그 기반/AMQP 이벤트/외부 푸시), (c) M2 최소 범위를 산출물로 제시한다.
## 비기능 요구사항
- NFR-1: 백필은 trex-core wire 스키마(slots RPC 필드)를 변경하지 않는다(값 정정만, 계약 불변).
- NFR-2: 백필 분기는 happy path(정상 평균가 수신) 성능·동작에 영향이 없어야 하며, 정상 경로에서는 기존과 동일하게 동작한다.
- NFR-3: 백필 가격은 추적 가능해야 한다 — 어떤 체결이 실제 평균가가 아니라 백필 근사로 기록됐는지 로그/플래그로 구분 가능해야 한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 체결가 None/0 → 지정가 백필(슬롯 평단 + 원장 price) + WARN 로깅 (FR-1~3).
- 알람 능력 검토 산출물(FR-4) — 검토/결정까지 In Scope.
- Out of Scope
- slots RPC 등 wire 계약 스키마 변경.
- 과거에 이미 0으로 기록된 원장/슬롯의 소급 정정(별도 데이터 보정 작업).
- {{DECIDE_ALARM_IMPL_SCOPE}} — 알람의 실제 구현 범위(검토 결과 M2 포함으로 결정되면 In Scope로 승격, 아니면 후속 이월).
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다.
## 리스크
- R-1 (Medium): 지정가 백필도 추정치라, 실제 체결가와 차이가 나면 원장 cost basis가 부정확해진다(0보다는 낫지만 정확하진 않음).
- 완화 방안: 백필을 WARN+플래그로 추적 가능하게 남기고, 런북에 "백필 항목은 근사" 명시. 필요 시 후속 정합화(잔고/주문 대조) 검토.
- R-2 (Medium): 백필 시점을 adapter(kis.py)로 둘지 application(execution.py)로 둘지에 따라 paper 브로커·재구성(dcy-021) 경로와의 일관성이 달라질 수 있다.
- 완화 방안: 백필 결정 지점을 단일화(application 권장 — 주문 컨텍스트/limit_price 접근 용이)하고 design에서 paper/재구성 경로 회귀 테스트.
- R-3 (Low): 알람 채널 도입이 범위를 키워 M2 일정을 압박할 수 있다.
- 완화 방안: FR-4를 "검토 산출물"로 제한하고, 구현은 검토 결과에 따라 별도 결정(OQ-1).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: orb 매수는 지정가 주문이라 대다수 케이스에서 `slot.limit_price`(또는 주문 지정가)가 존재한다.
- A-2: KIS 평균가 누락은 드문 순간 레이스이며, 정상 happy path에서는 평균가가 정상 수신된다(저빈도·고영향 특성).
- A-3: trex-core 기존 watchdog/원장 outbox/이벤트 발행 인프라는 그대로 재사용 가능하며, 본 변경은 그 위의 값 무결성 가드다.
## 오픈 질문
답을 아직 모르는 항목을 적는다.
- OQ-1: 알람(alert) 능력을 M2에 실제 구현까지 포함할지, 검토 산출물만 내고 구현은 후속으로 이월할지. 채널은 무엇으로 할지(구조화 로그 + 로그 기반 알람 / AMQP 이벤트 / 외부 푸시).
- 상태: Deferred (→ design)
- 처리 방안: design 단계에서 trex-core-dev가 채널별 장단점·최소 범위 분석 산출물 작성. 그 분석을 근거로 **알람의 M2 구현 포함 여부는 design review에서 사장님이 최종 결정**(범위·일정 결재 사안). FR-3 WARN 로깅은 채택 여부와 무관하게 M2 필수.
- 종료 조건: design review에서 알람 구현이 In Scope/후속 이월 중 하나로 확정되면 Answered (사장님 2026-06-21: design 분석 후 확정 합의).
- OQ-2: 주문 지정가(limit_price)마저 부재하거나 ≤0인 경우의 최종 백필 폴백 정책(예: 직전 종가/현재가 조회 / 해당 체결 보류·재폴링 / 기록 skip+강한 알림 중 무엇).
- 상태: Deferred (→ design)
- 처리 방안: design 단계에서 trex-core-dev가 후보별 영향 분석, 팀장 승인.
- 종료 조건: 폴백 우선순위가 단일 정책으로 확정되면 Answered (사장님 2026-06-21: design 분석 후 확정 합의).
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다.
- S-1: 평균가 None/0 체결 입력에 대해 슬롯 평단과 원장 price가 0이 아닌 지정가 백필 값으로 기록된다.
- S-2: 백필/0가격 체결 통과 시 식별 가능한 WARN 로그가 100% 남는다(무경고 통과 0건).
- S-3: 정상 평균가 수신 경로는 기존과 동일하게 동작한다(회귀 0건). 알람 능력 검토 산출물이 confirm-discovery에 제출된다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: 평균가 None/0을 주입한 단위/통합 테스트로 슬롯 평단·ledger_outbox price 검증 / 데이터 출처: 신규 테스트 + ledger_outbox 레코드.
- S-2 측정 방식: 백필 경로 테스트에서 WARN 로그 발생 단언 / 데이터 출처: 구조화 로그.
- S-3 측정 방식: 기존 execution/ledger 테스트 스위트 회귀 + 검토 문서 제출 여부 / 데이터 출처: 테스트 결과 + Discovery 첨부 산출물.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 알람 구현 범위(OQ-1)가 In Scope/이월 중 하나로 확정 — FR-1~3는 M2 In Scope 확정, 알람 구현 범위는 design review로 Deferred(설계 분석 후 사장님 결정)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3 측정안에 사장님/팀장 이견 없음
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 현재 High 리스크 없음(R-1/R-2 Medium) — design에서 R-2 결정 지점 단일화 책임자(trex-core-dev) 지정
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료 — OQ-1/OQ-2 모두 design Deferred(사장님 2026-06-21)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: confirm-discovery 시 팀장이 CONFIRMED_BY로 기록 — 팀장(Claude)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-032_20260621_fill-missing-price-cost-basis-backfill.md
- 참조 문서:
- docs/discovery/dcy-026_20260612_sell-transaction-tax-and-fee-policy.md (체결 기록·세금/수수료 인접 정책)
- docs/discovery/dcy-029_20260616_operational-observability-logging-and-pnl-view.md (운영 관측성·실현 PnL 정의)
- trex_core/application/execution.py, trex_core/adapters/broker/kis.py, trex_core/application/operator_report.py (현재 구현 근거)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
## DECIDE
- (해소→design) 알람 능력 M2 구현 포함 여부 + 채널 선택(OQ-1) — design 분석 산출물 근거로 design review에서 사장님 결정(사장님 2026-06-21).
- (해소→design) 지정가 부재 시 최종 백필 폴백 정책(OQ-2) — design 단계 trex-core-dev 분석 후 팀장 확정(사장님 2026-06-21).
## CONFIRM
- (확정) 본 건 FR-1~3(무결성 가드: 지정가 백필 + 로깅)의 M2 포함 — 사장님 결정(2026-06-21).
## DATA
- {{DATA_KIS_AVG_MISSING_FREQUENCY}} — 실거래에서 KIS 평균가 누락이 실제로 관측된 빈도(있다면 운영 로그/사례). 백필 폴백 정책 결정의 근거 보강용. (없으면 저빈도·고영향 가정으로 진행)
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: FR-1~3(지정가 백필+로깅) M2 포함 사장님 확정. OQ-1(알람 구현범위/채널)·OQ-2(지정가 부재 폴백) 모두 design 단계 trex-core-dev 분석 후 확정으로 Deferred 처리(사장님 2026-06-21) → Open OQ 0건. REQ 초안 작성 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: 팀장(Claude)
- Confirmed At (KST): 2026-06-21 (KST)
@@ -1,219 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-033_20260621_suite-alarm-delivery-notifier
- 이슈명: 제품군 알람 전달 체계 — 모듈 발신 + 중앙 notifier(trex-notifier) → 텔레그램(P1 아웃바운드)
- 작성 시각(KST): 2026-06-21 (KST)
- 마지막 갱신 시각(KST): 2026-06-21 (KST, draft-req trex-core-dev)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조:
- trex-core: REQ-098 (알람 envelope wire 계약), REQ-099 (trex.alerts exchange 토폴로지), REQ-100 (core CRITICAL emitter 배선), REQ-101 (알람 발신 흐름 baseline 정합) — 모두 Approved
- (cross-repo, 모듈 emitter/서비스 — 각 repo SRS) trex-orb-v1: REQ-039 / trex-tsl-v1: REQ-024 / trex-notifier: REQ-001~008 — Approved (confirm-req 팀장 2026-06-21). ※각 dev가 trex-core 범위 밖이라 미기재한 역참조를 팀장이 보강.
- ~~kis-data-hub: REQ-057~~ — **Deprecated (사장님 결정 2026-06-21: kdh는 당분간 trex-notifier 미사용 → 알람 범위 제외)**. 향후 재도입 시 새 REQ로 부활.
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: `/home/genichin/workspace/trex-lead/ALARM-INVENTORY.md`(0단계 알람 인벤토리 스윕 결과 — core/orb/tsl/kdh ~100개 조건, 본 Discovery의 source-of-truth), `repos/trex-notifier`(부트스트랩 완료된 신규 서비스 repo)
- 해석:
- TREX 제품군엔 운영자에게 이상을 즉시 알리는 구조화된 알람 채널이 없다. ~100개 알람 후보가 로그(또는 예외)로만 존재하고, CRITICAL 상황도 운영자가 능동 인지해야 한다. M2(tsl 매도 라이브)에서 운영자가 첫 거래일을 수기 감시해야 하는 부담도 이 갭에서 나온다.
- 토론으로 큰 방향은 확정됐다: 신규 `trex-notifier`가 알람을 중앙 수집해 텔레그램으로 전달(P1 아웃바운드만). 본 Discovery는 그 알람 **전달 계약과 아키텍처**를 trex-core 변경 단위로 정식화한다(envelope는 trex-core 소유 wire 계약).
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- dcy-029(운영 관측성 — 로깅/PnL 뷰), dcy-031(이벤트 발행 신뢰성)은 인접하나 "이상을 운영자에게 능동 푸시"하는 알람 전달 체계는 어디서도 다루지 않았다. 기존은 전부 pull(로그/CLI) 기반이다.
- 제품군 횡단 신규 컴포넌트(trex-notifier)와 신규 wire 계약(알람 envelope)을 도입하므로 독립 주기로 추적하는 편이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다.
- 현재 상태
- trex-core에 구조화 알람 채널 부재 — 운영자 표면은 CLI `trex-core report` + 구조화 로그(Loki)뿐(`daemon.py:248`에 alert 주석 1건). 푸시 메커니즘 없음.
- 인벤토리 스윕(ALARM-INVENTORY.md) 결과: core/orb/tsl/kdh에 ~100개 알람 후보. 대부분 WARN/ERROR로 로깅되나 **일부 CRITICAL은 예외 raise만 되고 로그조차 없다**(예: 토큰 격리, RPC 타임아웃 누적). 등급도 모듈마다 제각각.
- 연쇄/상관 실재: kis-data-hub 인증실패/다운 1건 → orb `kdh_auth_failure` + tsl `kis_auth_failure_abort` 동시 발생. 단 core safety-sell은 직접 KIS(다른 키)라 독립.
- `trex-notifier` repo는 부트스트랩 완료(골격+거버넌스+baseline, root commit `078d37d`, private). 단 전송 방식·의존성은 본 Discovery 확정까지 비어 있다.
- 기대 상태
- 각 모듈이 이상 조건을 표준 **알람 envelope**로 발신하면, 중앙 `trex-notifier`가 등급 판정·중복/상관 억제 후 CRITICAL은 텔레그램 즉시 푸시, WARN은 일일 다이제스트로 전달한다.
- 크래시·정지로 자기 알람을 못 내는 데몬은 **하트비트(침묵=장애)** 로 탐지된다.
- 알람 전달이 기존 wire 계약(slots RPC/trex.events)·로그 저장 경로를 건드리지 않는다(가산·격리).
- 이해관계자
- 사장님 — 운영자(알람 최종 수신자), 범위·보안·외부노출 결재자
- 팀장(Claude) — Discovery/게이트, 인벤토리 작성자
- trex-core-dev — envelope 계약 + core emitter delivery
- 각 모듈 dev(orb/tsl/kdh) — 모듈별 emitter delivery
- service-dev — trex-notifier 서비스 delivery
- 제약사항
- 기존 wire 계약(slots RPC, trex.events) 스키마 변경 금지 — 알람은 가산적 신규 채널로만.
- orb/tsl/kdh는 `trex_core`를 import하지 않는다 — 공유는 **규약(envelope)** 이지 런타임 코드가 아니다(모듈당 thin emitter).
- notifier는 로그 firehose 게이트웨이가 아니다 — 로그 저장은 인프라 슈퍼(Vector/Promtail→Loki) 담당. notifier는 알람만.
- core는 텔레그램을 직접 호출하지 않는다(외부 의존·시크릿 격리). 시크릿(봇 토큰)은 notifier 1곳.
- 알람 채널이 의존하는 인프라(RabbitMQ/Loki)가 장애나면 그 경로 알람이 못 나간다 → 하트비트 필요.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자: tsl 매도 라이브 첫날, `sell_signal_rejected_terminal`(포지션 못 팜) 발생 → 즉시 텔레그램 CRITICAL 푸시로 인지하고 개입. 현재는 로그를 들여다봐야만 알 수 있다.
- 운영자: kis-data-hub 인증 만료 → orb·tsl 동시 알람을 notifier가 "시세 허브 장애 — 2개 모듈 영향" 1건으로 묶어 전달(폭풍 억제).
- 운영자: tsl 데몬이 크래시해 침묵 → 하트비트 부재로 notifier가 "tsl 데몬 응답 없음" 알람.
- 운영자: 장마감 후 WARN 다이제스트("오늘 백필 2건, watchdog 인수 0건, stale 양보 0건")로 비긴급 이상을 한 번에 검토.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다.
- 변경 1 — **알람 envelope 계약(trex-core 소유)**: `source / severity(critical·warn·info) / code / title / fields / occurred_at / schema_version` 표준 메시지 스키마 정의. 전 모듈이 이 규약으로 발신.
- 변경 2 — **알람 전송 채널**: 전용 **`trex.alerts` AMQP exchange**(확정, 사장님 2026-06-21) — 가산·격리(기존 slots RPC/trex.events 불변). 모듈은 기존 RabbitMQ 연결로 발행, notifier가 단일 소비자.
- 변경 3 — **모듈별 emitter**: core/orb/tsl/kdh 각 repo에 thin 알람 발신기(기존 연결 재사용, ~십수 줄). 인벤토리 CRITICAL 집합부터 배선.
- 변경 4 — **trex-notifier P1**: 알람 수집 → 등급 판정 → 중복/상관 억제 → 텔레그램(CRITICAL 즉시 / WARN 다이제스트) + 하트비트.
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시한다.
- 코드 영향:
- trex-core: 알람 emitter(신규) + `trex.alerts` 채널 선언(채택 시). 기존 execution/safety_sell 등의 CRITICAL 지점에서 emit 호출.
- trex-orb-v1 / trex-tsl-v1 / kis-data-hub: 각 thin emitter + 주요 알람 지점 emit 호출(각 repo 별 batch).
- trex-notifier: classifier/deduplicator/sender/heartbeat 구현 + 진입점 + 의존성(텔레그램·AMQP 라이브러리) 확정.
- 문서 영향:
- trex-core `docs/interface-contract.md` — 알람 envelope + `trex.alerts` 채널 계약 신설.
- trex-core `docs/runtime-flows.md` — 알람 발신 흐름 추가(baseline 갱신).
- trex-notifier `docs/runtime-flows.md` — TBD였던 전송 방식 확정 반영.
- `docs/discovery/index.md` 갱신.
- 운영 영향:
- 운영자에게 텔레그램 알람 수신 채널·등급 정책이 신규 운영 절차로 추가(런북).
- 봇 토큰·화이트리스트 chat id 운영 비밀로 관리(notifier 1곳).
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: 표준 알람 envelope 스키마(`source/severity/code/title/fields/occurred_at/schema_version`)를 trex-core가 wire 계약으로 정의하고 `docs/interface-contract.md`에 명시한다.
- FR-2: 각 모듈(core/orb/tsl/kdh)이 인벤토리 CRITICAL 집합(ALARM-INVENTORY.md §A)의 조건 발생 시 envelope로 알람을 발신한다.
- FR-3: trex-notifier가 알람을 수집해 등급별로 처리한다 — CRITICAL은 텔레그램 즉시 푸시, WARN은 일일 다이제스트, INFO는 제외(로그만).
- FR-4: trex-notifier가 동일 근본원인의 상관 알람을 묶고(예: 시세 허브 장애 → 다모듈), 반복 알람을 rate-limit/중복 억제한다.
- FR-5: trex-notifier가 하트비트(dead-man's switch)로 데몬 침묵=장애를 탐지·알린다.
- FR-6: trex-core `docs/runtime-flows.md`에 알람 발신 흐름을, trex-notifier `docs/runtime-flows.md`에 확정 전송 방식을 반영한다(baseline 갱신).
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 알람 채널은 **가산·격리** — 기존 wire 계약(slots RPC, trex.events) 스키마를 변경하지 않는다. orb/tsl/kdh 기존 동작 무회귀.
- NFR-2: 보안 — 텔레그램 봇 토큰 시크릿은 notifier 1곳에만, 수신 대상은 화이트리스트(사장님 chat id). core/orb/tsl/kdh에 외부 시크릿 유입 없음.
- NFR-3: 신뢰성 — 알람 경로(notifier/RabbitMQ)가 죽어도 모듈 본업(매매)과 로그 저장은 영향 없음. notifier는 SPOF가 아니다.
- NFR-4: 모듈측 비용 최소 — emitter는 thin(기존 연결 재사용), `trex_core` import 없이 envelope 규약만 공유.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 알람 envelope 계약 + 전송 채널 + 모듈 emitter(CRITICAL 집합) + trex-notifier P1(수집·등급·상관/중복억제·텔레그램·하트비트).
- Out of Scope
- **kis-data-hub emitter** — 사장님 결정(2026-06-21) kdh는 당분간 trex-notifier 미사용 → 알람 범위에서 제외(REQ-057 Deprecated). 모듈 emitter는 **core/orb/tsl만**. (허브 장애는 orb `kdh_auth_failure`·tsl `kis_auth_failure_abort`가 소비자 측에서 탐지하므로 가시성 부분 유지.) 향후 재도입 시 새 REQ.
- **인바운드 명령 게이트**(텔레그램으로 시스템 제어) — P2/P3 후속 별도 discovery(보안·인가·감사).
- **로그 저장/Loki 적재** — 인프라 슈퍼(Vector/Promtail) 담당. notifier 경유 금지.
- 인벤토리 ~100개 code 전량 배선 — CRITICAL 우선, WARN 다이제스트는 점진. INFO 제외.
- 후보 C(core가 외부 푸시 직접 호출), 기존 slots RPC/trex.events 스키마 변경.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다.
## 리스크
- R-1 (Medium): 알람 폭풍 — 장애 시 다모듈·매사이클 알람이 쏟아져 알람 피로 유발.
- 완화 방안: notifier 상관/중복억제 + rate-limit(FR-4) + 등급화(CRITICAL만 즉시 푸시).
- R-2 (Medium): 알람의 역설 — 알람 채널이 의존하는 인프라(RabbitMQ/notifier) 장애 시 그 경로 알람 미전달.
- 완화 방안: 하트비트(침묵=장애, FR-5). 매매 안전망(core safety-sell)은 알람과 독립이라 알람 실패가 거래 보호를 깨지 않음.
- R-3 (Low): 보안 — 봇 토큰 유출 시 알람 수신 노출(P1은 읽기 방향이라 제어 위험은 없음).
- 완화 방안: 토큰 1곳 격리(NFR-2), 화이트리스트, 인바운드 명령은 범위 밖(P3로 분리).
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 모듈들은 이미 RabbitMQ 연결을 보유(core=broker, orb/tsl=intent/slots RPC)하므로 AMQP emitter는 기존 연결 재사용으로 저비용이다.
- A-2: 알람은 저빈도 이벤트라 전용 채널의 트래픽·복잡도 부담이 작다.
- A-3: 인프라 로그 슈퍼(Vector/Promtail→Loki)는 알람과 별개로 존재/구성된다(로그 저장은 본 범위 밖).
- A-4: trex-notifier는 단일 컨테이너 내 bare 프로세스로 다른 모듈과 공존 배포된다.
## 오픈 질문
답을 아직 모르는 항목을 적는다.
- OQ-1: 모듈→notifier 알람 전송 방식 — **전용 `trex.alerts` AMQP exchange**(권고: 실시간·격리·소비자 상관억제) vs **로그-tee**(슈퍼가 alert 라인 tee, 모듈측 코드 0이나 Loki 의존·지연).
- 상태: Answered — **전용 `trex.alerts` AMQP exchange로 확정**(사장님 2026-06-21).
- 처리 방안: trex-notifier는 `trex.alerts` 소비자. 각 모듈은 기존 RabbitMQ 연결로 발행. 가산·격리(orb/tsl 기존 계약 불변). trex-notifier 의존성(AMQP 클라이언트)·runtime-flows에 반영.
- 종료 조건: 충족됨.
- OQ-2: P1 즉시-푸시(CRITICAL paging) 대상 code 집합 확정 — ALARM-INVENTORY.md §A 제안 집합을 사장님이 검토해 실제 폰을 울릴 항목을 확정.
- 상태: Answered — **ALARM-INVENTORY.md §A 전체(약 25개 code)를 P1 CRITICAL paging으로 채택**(사장님 2026-06-21). 항목 축소 없음 — 알람 폭풍은 항목 삭제가 아니라 notifier 상관/rate-limit(FR-4)으로 제어.
- 처리 방안: §A의 core/tsl/orb/kdh CRITICAL code 전체가 즉시-푸시 대상. delivery에서 각 모듈 emitter가 해당 code 배선.
- 종료 조건: 충족됨.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다.
- S-1: 확정된 CRITICAL 조건 발생 시 운영자 텔레그램에 알람이 도달한다(엔드투엔드).
- S-2: 동일 근본원인의 다모듈 알람이 1건으로 묶이고, 반복 알람이 rate-limit된다(폭풍 억제).
- S-3: 데몬 침묵(크래시/정지) 시 하트비트 부재로 장애가 탐지된다. 기존 wire 계약·매매 동작은 무회귀.
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: CRITICAL 조건 주입(테스트/시뮬) → 텔레그램 수신 확인 / 데이터 출처: notifier 로그 + 텔레그램 메시지.
- S-2 측정 방식: 다모듈 동시 알람·반복 알람 주입 → 묶임/억제 단언 / 데이터 출처: notifier 상관·rate-limit 테스트.
- S-3 측정 방식: 데몬 하트비트 중단 시뮬 → 장애 알람 발생 단언 + 기존 wire/매매 회귀 스위트 / 데이터 출처: notifier 테스트 + 각 모듈 회귀 테스트(interface-contract diff 0).
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: P1(아웃바운드 알람)만 In Scope, 명령 게이트·로그 저장 제외에 사장님 동의 — 확정(토론 합의)
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-3 측정안 합의
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: 현재 High 리스크 없음(R-1/R-2 Medium) — 폭풍억제·하트비트 책임자(notifier service-dev) 지정
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1(전송=`trex.alerts` AMQP)·OQ-2(paging=§A 전체) 모두 Answered(사장님 2026-06-21)
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: confirm-discovery 시 팀장이 CONFIRMED_BY로 기록 — 팀장(Claude)
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-033_20260621_suite-alarm-delivery-notifier.md
- 참조 문서:
- /home/genichin/workspace/trex-lead/ALARM-INVENTORY.md (인벤토리 스윕 — 현재상태/요구사항 근거)
- docs/discovery/dcy-029_20260616_operational-observability-logging-and-pnl-view.md (운영 관측성 인접)
- docs/interface-contract.md, docs/runtime-flows.md (envelope/채널 계약 + 발신 흐름 baseline 갱신 대상)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다.
## DECIDE
- (해소) 알람 전송 방식 = 전용 `trex.alerts` AMQP exchange — 사장님 확정(2026-06-21, OQ-1).
- (해소) P1 CRITICAL paging 집합 = ALARM-INVENTORY.md §A 전체(약 25개 code) — 사장님 확정(2026-06-21, OQ-2).
## CONFIRM
- (확정) P1(아웃바운드 알람)만 범위, 인바운드 명령 게이트·로그 저장은 후속 분리 — 사장님 확정(2026-06-21).
## DATA
- {{DATA_TELEGRAM_BOT}} — 텔레그램 봇 토큰 + 수신 chat id(화이트리스트). delivery 단계 필요(사장님 BotFather 생성).
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: confirm-discovery 완료. OQ-1(전송=`trex.alerts` AMQP)·OQ-2(paging=§A 전체) 모두 사장님 확정 → Open OQ 0건. 범위(P1 아웃바운드)·성공기준·리스크 책임자 확정. DATA(텔레그램 봇 토큰)는 e2e 단계 필요(비차단). **알람은 M2 포함 확정(사장님 2026-06-21) — delivery 착수.** (※이전 "M2 이후 큐잉"은 팀장 오판 정정. 원합의=가능하면 M2 포함이었고 BAT-048 완료로 착수 시점 도달. 알람은 best-effort·별도 채널이라 매매 위험 0.)
- Ready for REQ Drafting: true
- Confirmed By: 팀장(Claude)
- Confirmed At (KST): 2026-06-21 (KST)
@@ -1,228 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart
- 이슈명: 슬롯↔실계좌 차단형 reconcile + sync/verify 명령 — 중장기 cold-start (기존 보유 포지션 onboarding)
- 작성 시각(KST): 2026-06-25 (KST)
- 마지막 갱신 시각(KST): 2026-07-14 (KST, dcy-037 HOTFIX follow-up link)
- 대체됨: 없음
- 후속 Discovery 참조: `docs/discovery/dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix.md` (REQ-105/106 operator broker truth 및 managed non-holding divergence 운영 환류)
- 생성된 REQ 참조: REQ-102 ~ REQ-108 (draft-req 2026-06-25, Proposed — confirm-req 대기)
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub 보드 `fourblocks2020/trex` #7 항목 "trex-core: 슬롯↔실계좌 차단형 reconcile + sync/verify 명령 — 중장기 cold-start (M3)" (M3/P1, 설계방향 본문 보유). 워크스페이스 메모리 `m3-goal-midlong-core-instance`.
- 선행 Discovery: dcy-020(자산중심 슬롯), dcy-027(중장기 슬롯 운영·운영 프로파일), dcy-028(slot strategy-state store), dcy-029(슬롯/PnL 뷰). 본 건은 이들 위에 cold-start 동기화 갭을 정의한다.
- 해석:
- M3 메인 목표는 "중/장기 전략용 별도 trex-core 인스턴스 추가"(사장님 2026-06-25). 그 인스턴스는 **이미 실계좌에 보유 중인 중장기 종목 위에서 cold-start** 한다 — portfolio-hub에 종목·ledger(잔고)가 있고 브로커 계좌에 실보유가 있는데 **slot DB는 비어 있는 상태**에서 데몬이 처음 뜨는 상황이 정상 시나리오다.
- 현행 reconcile는 이 cold-start 갭에서 **탐지(WARN/error)만 하고 차단도 해소 경로도 없다.** 슬롯↔브로커·원장↔슬롯 불일치가 있어도 전략이 그대로 돌 수 있고(데이터 오염 위 매매 위험), 운영자가 불일치를 안전하게 정합화해 전략을 재개할 표준 명령이 없다.
- 본 Discovery는 이 갭을 "차단형 reconcile(A→B→C) + 운영자 sync/verify 명령 + 차단형 warning 상태"라는 trex-core 변경 단위로 정식화한다. (중장기 **전략 모듈 자체**·멀티데이 PnL·인스턴스 프로파일 신설은 별개 단위 — 범위 경계 참조.)
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 reconcile 자산은 **탐지 전용**이다. `reconcile_core_holdings`(REQ-062)는 A(슬롯 보유·브로커 0)만 ERROR 전이하고 B(브로커 보유·슬롯 없음)·C(수량 불일치)는 **WARN 로그만** 남긴다(`application/reconciliation.py:48-83`). `reconcile_positions`(REQ-031)는 portfolio-hub↔브로커 불일치 시 asset overlay `set_warning(pid)`를 켜지만 **in-memory·비차단**이며 docstring이 명시하듯 *"pause is the durable channel for manual holds"*(`reconciliation.py:1-8,118`).
- 즉 "불일치 시 전략을 **차단**하고, 운영자가 **명령으로 정합화해 차단을 해제**한다"는 행위는 어디에도 없다. dcy-027(중장기 슬롯 운영)은 운영 프로파일·멀티데이 리스크까지 다뤘으나 cold-start 시 기존 보유 onboarding·동기화 차단은 Out-of-Scope로 두었다.
- 신규 운영자 CLI 명령(sync/verify)과 차단형 슬롯 상태 도입은 dcy-016(운영자 CLI) 이후의 독립 능력 추가라 별도 주기로 추적하는 편이 명확하다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
- 현재 상태
- **cold-start 갭**: 중장기 인스턴스가 처음 뜰 때 slot DB는 비어 있다. boot시 `snapshot_scheduler.run_once()`가 ① whoami → ② get_portfolios → ③ `store.swap` → ④ `reconcile_positions`(portfolio-hub↔브로커) → ⑤ `_reconcile_slots`(`reconcile_universe`로 INACTIVE 슬롯 생성) → ⑥ `reconcile_core_holdings`(슬롯↔브로커) 순으로 돈다(`snapshot_scheduler.py:85-146`). 그 결과 portfolio-hub 자산은 **INACTIVE 슬롯으로 생성**되지만, 실계좌에 이미 보유 중인 수량은 슬롯에 **HOLDING으로 반영되지 않는다**(슬롯 생성 기준은 자산 리스트지 브로커 잔고가 아님). 브로커가 보유한 종목은 `reconcile_core_holdings`의 케이스 B("unmanaged position")로 **WARN 로그만** 남고 슬롯은 INACTIVE에 머문다.
- **차단 부재**: 위 불일치(B 미반영, C 수량 차이, asset warning)는 전략 수행을 막지 않는다. SlotState는 5상태(inactive/pending_buy/holding/pending_sell/error)뿐이고 "warning"은 슬롯 필드가 아니다(`domain/slot/state.py:30-37`). asset warning은 in-memory overlay라 재시작 시 사라지고 전략 read/intent 경로를 차단하지 않는다. 유일한 durable 차단 채널은 **pause**(portfolio_id 단위, `snapshot_pause` 테이블/pause_store)뿐인데, 이는 reconcile가 자동으로 걸지 않는 **수동 채널**이다.
- **해소 명령 부재**: 운영자가 "슬롯을 실계좌에 맞춰라"라고 지시할 표준 CLI가 없다. 현 `trex-core` CLI는 pause/resume/paused/universe/slots/enter/exit/cancel/report뿐이다(`runtime/cli.py`). 불일치를 손으로 정합화하려면 원장 수정(pfhub) 또는 슬롯 직접 손질에 의존해야 하고, 정합 후 "warning 해제·전략 재개"를 안전하게 확인하는 경로가 없다.
- **결과 위험**: 평단 0/수량 불일치 슬롯 위에서 중장기 전략이 매도·증액 결정을 내리면(예: tsl 류 평단 기반 tier, 사이징) 잘못된 cost basis로 실거래가 나간다. dcy-032에서 평단 0 오염의 하류 피해(tier 무시 폴백 매도, 실현손익 과대)를 이미 확인했다 — cold-start 미정합은 그 위험을 인스턴스 기동 시점에 구조적으로 만든다.
- 기대 상태
- 중장기 인스턴스 cold-start 시, portfolio-hub 자산은 슬롯 유니버스로 정합되고(A), 실계좌 보유는 **불일치로 탐지되면 해당 슬롯이 차단(전략 미수행) 상태**가 되어, 정합 전 잘못된 매매가 절대 나가지 않는다.
- 운영자는 표준 CLI로 (B) 슬롯을 브로커 잔고에 맞춰 동기화(`sync`)하고, (C) 원장↔슬롯 불일치는 portfolio-hub CLI로 원장을 고친 뒤 trex-core `verify`로 재확인해 차단을 해제한다. 강제 해제(force-clear) 없이 **재대조가 일치할 때만** 차단이 풀린다.
- 차단 채널이 pause(수동 hold)와 별개로 **reconcile 기반 자동 차단(warning)** 으로 durable하게 존재하고, 두 채널 중 하나라도 걸리면 전략이 차단된다.
- 이해관계자
- 사장님 — M3 범위·차단/해제 정책·비가역 운영(실계좌 동기화) 승인자
- 팀장(Claude) — Discovery/게이트 관리, cold-start 갭 추적자
- trex-core-dev — delivery 담당
- 운영자 — cold-start 시 sync/verify 수행 주체, 차단/해제의 최종 소비자
- 제약사항
- 브로커(KIS) 잔고가 reconcile의 ground truth다(invariant B, 단일 계좌). 슬롯/원장은 미러이며 권위는 브로커에 있다.
- **자동 무음 정정·자동 청산 금지**(issue #15 기조 유지). reconcile는 탐지+차단까지만 자동이고, 실제 정합(슬롯=브로커 반영, 원장 수정)은 **운영자 명령**으로만 일어난다.
- trex-core wire 계약(slots RPC 필드 등) 변경은 blast radius가 커 가급적 피한다. 차단 상태가 슬롯 read 표면에 노출돼야 한다면 그 스키마 영향은 design에서 최소화·명시한다.
- 단기 인스턴스(M1/M2 라이브: orb+tsl)에 회귀를 주지 않아야 한다 — 차단형 reconcile는 모든 인스턴스에 공통 적용되므로, 단기 인스턴스의 정상 운영(당일 슬롯·평소 일치 상태)에서 신규 차단이 오발동하지 않아야 한다.
## 주요 사용자 시나리오
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
- 운영자(cold-start onboarding): 중장기 인스턴스를 처음 띄운다. portfolio-hub에 A·B·C 3종목이 있고 실계좌에 이미 보유 중이다. 데몬 기동 → 슬롯 유니버스 생성 + 보유 불일치 탐지로 해당 슬롯 차단(전략 정지). 운영자가 `trex-core sync`로 슬롯을 실계좌 잔고에 맞추고, 일치 확인 후 차단이 풀려 전략이 비로소 돈다.
- 운영자(원장↔슬롯 불일치, C): reconcile가 원장(portfolio-hub 스냅샷)과 슬롯/브로커 간 수량 불일치를 차단으로 표면화. 운영자가 `pfhub portfolio buy/sell/invest`로 원장을 정정 → `trex-core verify <pid>`로 재대조 → 일치 시 차단 해제, 여전히 불일치면 해제 거부(사유 출력).
- 운영자(직접 매매 phantom, B 주의): 운영자가 KIS HTS로 직접 사고판 종목이 슬롯 없이 브로커에만 있다. `sync`가 이를 슬롯에 반영할 때 방향(매니지 대상으로 편입할지/무시할지)을 잘못 잡으면 중장기 전략이 의도치 않은 포지션을 떠안는다 — 방향 정책 확인이 필요하다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
- 변경 1 — 차단형 reconcile(A→B→C): boot/일일 reconcile에서 불일치를 탐지하면 **해당 슬롯을 차단 상태로 전이**한다. A=portfolio-hub 자산→슬롯 유니버스 정합(기존 유지), B=슬롯↔브로커 수량 불일치→슬롯 차단, C=원장(portfolio-hub 스냅샷)↔슬롯 수량 불일치→슬롯 차단. 차단된 슬롯은 전략 수행(매수/매도/사이징 intent) 대상에서 제외된다.
- 변경 2 — 차단형 warning 상태 도입: pause(수동·durable)와 **별개의 reconcile 기반 차단 상태(warning)** 를 durable하게 도입하고, pause OR warning 중 하나라도 걸리면 전략을 차단하도록 차단 게이트를 확장한다. (저장 위치·표현 방식은 OQ.)
- 변경 3 — 운영자 sync 명령: `trex-core sync`(신규) — 슬롯 수량을 브로커 잔고(ground truth)에 맞춰 반영하고, 일치하면 해당 슬롯의 reconcile-warning을 해제한다. cold-start 시 기존 보유의 슬롯 onboarding 경로이기도 하다.
- 변경 4 — 운영자 verify 명령: `trex-core verify`(신규) — C(원장↔슬롯) 불일치로 차단된 슬롯에 대해, 운영자가 portfolio-hub CLI로 원장을 정정한 뒤 **재대조**해 일치 시 warning을 해제, 불일치면 사유와 함께 해제를 거부한다(force-clear 없음).
## 영향 범위
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
- 코드 영향:
- `trex_core/application/reconciliation.py` — `reconcile_core_holdings`(B/C를 WARN→슬롯 차단으로 승격), `reconcile_positions`(asset warning을 durable 차단으로 연계할지 결정).
- `trex_core/application/snapshot_scheduler.py` — reconcile 순서/차단 적용 지점(`run_once`).
- 슬롯 도메인/저장소 — 차단형 warning 상태의 저장(슬롯 필드 신설 vs pause류 별도 store vs 신규 SlotState). `domain/slot/state.py`, slot repo.
- 전략 차단 게이트 — slots RPC read 표면/ intent 수용 경로(`application/messaging_handlers.py`)에서 pause에 더해 warning도 차단으로 반영.
- `trex_core/runtime/cli.py` — `sync`/`verify` 서브커맨드(데몬 RPC) 추가.
- 문서 영향:
- `docs/discovery/index.md` 신규 행 추가.
- `docs/runtime-flows.md`(reconcile/차단/해제 흐름), `docs/operations.md`(cold-start onboarding 런북) 갱신.
- 운영 영향:
- 중장기 인스턴스 cold-start 표준 절차 신설(기동→차단 확인→sync/verify→재개).
- 차단/해제가 운영자 명령 의존이므로 런북·교육 필요. pfhub 원장 정정과 trex-core verify의 2-서비스 협응 절차 명시.
# 5. 요구사항 목록
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
## 기능 요구사항
- FR-1: boot/일일 reconcile에서 슬롯↔브로커(B) 또는 원장↔슬롯(C) 불일치가 탐지되면 해당 슬롯을 **차단 상태(reconcile-warning)** 로 전이하고, 차단된 슬롯은 전략 read 표면/intent 수용에서 제외된다.
- FR-2: 전략 차단 게이트는 pause(수동) **또는** reconcile-warning 중 하나라도 걸리면 해당 슬롯/포트폴리오의 전략 수행을 차단한다.
- FR-3: `trex-core sync` 명령(별도 명령, OQ-2)은 **브로커 잔고(정본)를 유지한 채 대상 슬롯의 보유 수량을 브로커에 맞춰 갱신**하고(슬롯 미러를 브로커로 수렴, INACTIVE→HOLDING 전이 포함), 정합 후 일치하면 그 슬롯의 reconcile-warning을 해제한다. cold-start 시 portfolio-hub 유니버스에 있어 슬롯이 생성된 보유 종목(B)이 그 대상이다. **브로커에만 있고 portfolio-hub에 없는 종목(슬롯 부재)은 sync 대상이 아니다** — unmanaged 경고로 유지하며 자동 슬롯 생성하지 않는다(슬롯 생성 기준=portfolio-hub 유니버스, OQ-3).
- FR-4: `trex-core verify` 명령(별도 명령, read-중심, OQ-2)은 C로 차단된 슬롯을 재대조해, 일치하면 warning을 해제하고 불일치면 사유와 함께 해제를 거부한다(강제 해제 없음). 슬롯 수량을 변경하지 않는다(운영자가 portfolio-hub CLI로 원장을 이미 정정).
- FR-5: reconcile-warning 차단 상태는 **pause_store와 동형의 별도 durable store**(slot_id/pid 키)에 저장한다 — SlotState enum·전이표·slots RPC wire 계약을 변경하지 않는다(OQ-1).
- FR-6: 차단/해제 전이는 식별 가능한 로그(슬롯·심볼·원인 케이스·수량 before/after·actor)를 남기고, 고영향 불일치(예: A phantom)는 기존 알람 채널(dcy-033/trex.alerts)로 통지한다.
- FR-7(교차 서비스 의존): cold-start cost basis seed를 위해 **portfolio-hub에 "현재 보유 평단(WAC) read" 엔드포인트를 신규 추가**하고(→ **portfolio-hub `dcy-008` / `REQ-043` GET /v1/portfolios/{id}/holdings/cost-basis**, 2026-06-25 confirm), trex-core sync/onboarding가 이를 읽어 슬롯 평단을 seed한다(OQ-4). 현행 portfolio-hub는 미매도 보유분 평단 read가 없음(`pnl/realized` 매도행 `cost_basis_per_unit`만 노출). REQ-105(sync)는 이 read 가용 시 평단 seed, 미가용 시 수량만 갱신+WARN으로 graceful degrade.
## 비기능 요구사항
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
- NFR-1: 차단형 warning 상태는 **durable**(재시작 후에도 유지)해야 한다 — in-memory overlay로 두지 않는다.
- NFR-2: 단기 인스턴스(M1/M2 라이브)의 정상(일치) 운영에서 신규 차단이 오발동하지 않아야 한다(회귀 0). 차단은 실제 불일치에서만 발생한다.
- NFR-3: reconcile/차단은 무음 자동 정정·자동 청산을 하지 않는다(issue #15 기조). 정합 행위는 운영자 명령으로만 일어난다.
- NFR-4: 가능하면 wire 계약(slots RPC 스키마) 변경을 피하거나 최소화한다. 차단 상태 노출이 필요하면 영향 범위를 design에서 명시한다.
## 범위 경계
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
- In Scope
- 차단형 reconcile(B/C → 슬롯 차단), 차단형 warning durable 상태(별도 store, OQ-1), pause∨warning 차단 게이트.
- `trex-core sync` / `trex-core verify` 운영자 명령(별도 2명령, OQ-2) + cold-start onboarding 경로.
- 차단/해제 로깅 + 고영향 알람 연계, cold-start 운영 런북.
- (교차 서비스 의존, FR-7) portfolio-hub "현재 보유 평단(WAC) read" 엔드포인트 신규 추가 — cold-start cost basis seed용. portfolio-hub-dev 별도 REQ로 발주(trex-core REQ의 선행 의존).
- Out of Scope
- 중장기 **전략 모듈 자체**(매매 로직)와 중장기 인스턴스 TOML 프로파일 신설 — 별개 단위(dcy-027 기반 후속).
- 멀티데이 PnL 리포트 — portfolio-hub 실현 PnL API(REQ-040/041)로 별도 처리(보드 M3 항목).
- 과거 0/불일치로 기록된 원장·슬롯의 소급 일괄 정정.
- 실시간(틱 단위) 슬롯↔계좌 동기화 — 본 건은 boot/일일 reconcile + 운영자 명령 시점 동기화에 한정.
# 6. 리스크/가정 목록
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
## 리스크
- R-1 (Medium, OQ-3로 완화됨): B 동기화 방향 오판 — 운영자 직접 매매로 생긴 phantom 포지션을 `sync`가 무분별하게 슬롯에 편입하면 중장기 전략이 의도치 않은 포지션을 관리·매도하게 된다.
- 완화 방안(확정): `sync`는 **portfolio-hub 유니버스에 있어 슬롯이 생성된 종목만** 대상으로 하고 슬롯 qty를 브로커로 수렴한다. 브로커-only(portfolio-hub 부재, 슬롯 없음) 포지션은 sync가 건드리지 않고 unmanaged 경고로 유지 → phantom 자동 편입 경로가 구조적으로 차단된다(OQ-3). 추가로 sync는 종목/슬롯 명시 대상으로 한정(전역 일괄 금지), 적용 전 before/after 출력.
- R-2 (Medium): 차단 게이트 확장이 단기 인스턴스에 회귀를 줄 수 있다(정상 상태인데 차단).
- 완화 방안: 차단은 실제 수량 불일치에서만 발생하도록 조건을 좁히고, 단기 인스턴스 시나리오(당일 슬롯·일치) 회귀 테스트를 design/verification에 포함.
- R-3 (Medium): 차단형 warning 저장 위치 선택(슬롯 필드 vs 별도 store vs 신규 SlotState)이 slots RPC wire 계약·전이표(`state.py`)에 파급될 수 있다.
- 완화 방안: pause(별도 store, 비침습)를 선례로 우선 검토. wire 영향 최소 안을 design에서 비교.
- R-4 (Medium): cold-start 시 평단(cost basis)을 어디서 가져올지 — 브로커 잔고는 수량은 주지만 중장기 전략이 쓸 평단/취득가를 슬롯에 seed해야 한다.
- 완화 방안: portfolio-hub 단가/평단 read 가용성 확인(OQ). 부재 시 폴백(브로커 평균가/원장 재구성) 정책 design.
## 가정
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
- A-1: 중장기 인스턴스는 기존 multi-instance 토대(dcy-009/BAT-013, instance_key별 큐/락/TOML)를 그대로 재사용한다. 본 건은 그 위의 cold-start 동기화 능력이다.
- A-2: 브로커 `get_balance()`가 cold-start 시점에 실보유 수량을 신뢰 가능하게 반환한다(ground truth).
- A-3: portfolio-hub에 중장기 종목과 ledger(잔고)가 선행 등록되어 있다(자산 리스트가 슬롯 유니버스의 기준).
- A-4: pause가 durable 차단의 검증된 선례이므로, reconcile-warning도 유사한 durable·비침습 저장 방식을 취할 수 있다.
## 오픈 질문 (전부 Answered — 사장님 2026-06-25 confirm-discovery)
모든 OQ가 confirm-discovery에서 확정되었다. 결정 결과는 아래에 resolved 기록으로 남기고 본문(FR-3~7, 변경 1~4, R-1)에 반영했다.
- OQ-1 (Answered): 차단형 warning 상태 저장 위치 → **(b) pause_store와 동형의 별도 durable store**(slot_id/pid 키). SlotState enum·전이표·slots RPC wire 계약 불변(blast radius 최소). 반영: FR-5.
- OQ-2 (Answered): 명령 표면 → **별도 2명령 `sync`/`verify`**(단일 --apply 통합 안 채택 안 함 — --apply 오타 시 의도치 않은 슬롯 변경 위험). sync=쓰기(슬롯 qty←브로커), verify=read-중심 재대조. 반영: FR-3/FR-4.
- OQ-3 (Answered): B 동기화 방향 → **브로커 보유 정본 유지, 슬롯 보유량을 브로커에 맞춰 갱신.** 슬롯이 있는 종목(portfolio-hub 유니버스 기반)만 sync 대상. 브로커-only(portfolio-hub 부재)는 슬롯 자동생성 안 하고 unmanaged 경고 유지(슬롯 생성 기준=portfolio-hub). 반영: FR-3, R-1.
- OQ-4 (Answered): cold-start 평단 seed → **portfolio-hub에 "현재 보유 평단(WAC) read" 엔드포인트 신규 추가** 후 trex-core가 읽어 seed. 교차 서비스 의존(별도 portfolio-hub REQ). 근거: 현행 portfolio-hub는 WAC를 내부 계산하나(`app/domain/pnl.py:calculate_realized_pnl` running_wac) 외부 노출은 `GET /v1/portfolios/{id}/pnl/realized`(REQ-040) 매도행 `cost_basis_per_unit`뿐 — 미매도 보유분 현재 평단 read 부재(팀장 확인 2026-06-25). 반영: FR-7. 브로커 평균가 단독 의존은 dcy-032(평균가 0/None 케이스)로 위험.
- OQ-5 (Answered): C(원장↔슬롯) 대조 정의 → **현행 브로커 경유 2-대조 재사용**(브로커=단일 정본 일관). reconcile_positions(원장↔브로커)+reconcile_core_holdings(슬롯↔브로커) 양쪽을 브로커 기준으로 보고 불일치에 차단만 입힌다. B sync로 슬롯=브로커가 되면 원장↔슬롯 불일치=원장↔브로커 불일치로 환원되어 verify는 reconcile_positions 재실행으로 판정 가능. 반영: 변경 1, FR-4.
# 7. 초기 성공 기준
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
- S-1: 보유 종목이 실계좌에 있고 slot DB가 빈 cold-start 입력에서, 데몬 기동 후 불일치 슬롯이 **차단 상태**가 되고 전략 intent가 발행되지 않는다.
- S-2: `trex-core sync` 후 슬롯 수량이 브로커 잔고와 일치하고 해당 슬롯의 reconcile-warning이 해제되어 전략이 재개된다.
- S-3: C로 차단된 슬롯은 원장 정정 전 `verify`가 해제를 거부하고, 원장 정정 후 `verify`가 해제한다(강제 해제 경로 없음).
- S-4: 단기 인스턴스 정상(일치) 시나리오에서 신규 차단이 발생하지 않는다(회귀 0).
## 측정 방식 및 데이터 출처
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
- S-1 측정 방식: cold-start 픽스처(빈 slot DB + 브로커 보유 + portfolio-hub 자산) 통합 테스트로 슬롯 차단 상태·intent 미발행 검증 / 데이터 출처: 신규 테스트.
- S-2 측정 방식: sync 명령 테스트로 슬롯 수량=브로커, warning 해제, 전략 게이트 통과 검증 / 데이터 출처: 슬롯 repo + RPC 응답.
- S-3 측정 방식: 원장 불일치 상태에서 verify 거부 → 정정 후 verify 해제 시나리오 테스트 / 데이터 출처: 신규 테스트 + 로그.
- S-4 측정 방식: 기존 단기 reconcile/실행 테스트 스위트 회귀 / 데이터 출처: 테스트 결과.
# 8. REQ로 넘기기 전 확인 체크
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 전략 모듈/인스턴스 프로파일/멀티데이 PnL을 본 건에서 제외하는 데 사장님 합의 — M3 framing(메모리 m3-goal-midlong-core-instance)상 전략 모듈은 별개 단위, 멀티데이 PnL은 portfolio-hub REQ-040/041로 분리 확정(사장님 2026-06-25). portfolio-hub 평단 read만 교차 의존으로 In Scope 편입.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 측정안에 사장님/팀장 이견 없음
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(B 동기화 방향)은 OQ-3 확정(슬롯 qty←브로커, 브로커-only 자동편입 금지)으로 Medium으로 완화. 해소 책임=trex-core-dev(sync 구현 시 대상 한정·before/after 출력).
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1~5 전부 Answered(사장님 2026-06-25). Open 0건.
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 팀장(Claude)이 CONFIRMED_BY로 기록.
# 9. 파일 처리 결과
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart.md
- 참조 문서:
- docs/discovery/dcy-020_20260610_asset-centric-slot-model.md (자산중심 슬롯·유니버스 reconcile)
- docs/discovery/dcy-027_20260612_long-term-strategy-slot-operation.md (중장기 슬롯 운영·운영 프로파일, cold-start Out-of-Scope 이월원)
- docs/discovery/dcy-028_20260615_slot-strategy-state-store.md (slot strategy-state — onboarding 시 seed 대상)
- trex_core/application/reconciliation.py, trex_core/application/snapshot_scheduler.py, trex_core/runtime/cli.py, trex_core/domain/slot/state.py (현재 구현 근거)
- GitHub 보드 #7 "슬롯↔실계좌 차단형 reconcile + sync/verify" 항목(설계방향 본문)
# 10. 사용자 결정 필요 항목 요약
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
## DECIDE (전부 결정됨 — 사장님 2026-06-25)
- (확정) OQ-3 B 동기화 방향 → 브로커 정본 유지·슬롯 qty를 브로커로 수렴. 브로커-only(portfolio-hub 부재) 자동 슬롯 편입 금지(unmanaged 경고 유지).
- (확정) 범위 경계 → 중장기 전략 모듈·인스턴스 TOML 프로파일·멀티데이 PnL은 본 건에서 제외(별개 단위). portfolio-hub 평단 read는 교차 의존으로 In Scope.
- (확정) OQ-1 warning 저장=별도 durable store / OQ-2 명령=별도 2명령 sync,verify / OQ-5 C대조=현행 브로커 경유 2-대조 재사용.
## CONFIRM (확정)
- 본 Discovery 핵심 방향(차단형 reconcile + sync/verify + durable warning)을 M3 cold-start 단위로 채택 — 사장님 2026-06-25.
- pause∨warning 차단 게이트로 전략 차단 일원화 — 채택.
## DATA (확보됨)
- (OQ-4 확인 완료) portfolio-hub는 미매도 보유분 현재 평단 read 부재 → "현재 보유 평단(WAC) read" 신규 추가로 결정(FR-7, 교차 서비스 의존 REQ).
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: 핵심 방향(차단형 reconcile + sync/verify + durable warning) 채택, OQ-1~5 전부 Answered(사장님 2026-06-25), High 리스크(R-1) OQ-3 확정으로 Medium 완화. portfolio-hub 평단 read(FR-7)는 교차 서비스 선행 의존으로 명시 — draft-req 시 trex-core REQ와 portfolio-hub 의존 REQ를 함께 분해. Open OQ 0건 → 인계 가능.
- Ready for REQ Drafting: true
- Confirmed By: 팀장(Claude)
- Confirmed At (KST): 2026-06-25 (KST)
# 12. 작성 가이드
- 이 문서는 dcy 템플릿 기반으로 작성되었으며, Open Questions는 가정으로 대체하지 않고 그대로 유지한다.
- confirm-discovery 시 OQ-1~5의 처리(Answered/Deferred)와 Freeze 플래그를 갱신한다.
@@ -1,154 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-035_20260626_multiday-realized-pnl-report-wac
- 이슈명: 중장기 운영 리포트 — portfolio-hub WAC 실현PnL(/pnl/realized) 연동 (멀티데이 정확 실현손익)
- 작성 시각(KST): 2026-06-26 (KST)
- 마지막 갱신 시각(KST): 2026-06-26 (KST)
- 대체됨: 없음
- 후속 Discovery 참조: 없음
- 생성된 REQ 참조: REQ-109, REQ-110, REQ-111
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 TODO/메모: GitHub 보드 #7 "중장기 운영 리포트 — portfolio-hub WAC 실현PnL API(/pnl/realized) 연동 (멀티데이, M3)". M3 중장기 인스턴스 운영 도구(슬롯 동기화 dcy-034의 리포트 짝).
- 선행: dcy-029(운영 관측성·실현 PnL 뷰, REQ-088 operator report), dcy-032(체결가 누락→cost basis 오염), portfolio-hub dcy-007/REQ-040(WAC 실현 PnL API).
- 해석:
- 현행 trex-core 운영 리포트(`application/operator_report.py`, REQ-088/dcy-029)는 실현 PnL을 **당일 net 매매 현금흐름**(`Σ매도(qty·price−fee−tax) − Σ매수(qty·price+fee)`)으로 계산한다. **일중 진입·청산 전략에선 net ≈ 실현손익**이라 충분했으나(M1/M2 단기), **중장기(수일~수개월 보유)에선 cost basis(WAC)를 무시한 당일 net이 실현손익과 다르다**.
- portfolio-hub는 이미 **WAC 기반 실현 PnL**(`GET /v1/portfolios/{id}/pnl/realized`, REQ-040/041: `(매도가−running_wac)·qty−fee−tax`)을 제공한다. M3 중장기 인스턴스의 운영 리포트는 이 WAC API를 소비해 **멀티데이 정확 실현손익**을 보여야 한다.
- 본 Discovery는 그 연동을 trex-core 변경 단위로 정식화한다(portfolio-hub는 기존 API 그대로 — 무변경).
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- dcy-029(REQ-088)는 **당일 net** 실현 PnL을 정의했다(`operator_report.py` 주석: "일중 진입·청산 전략에서는 net ≈ 실현손익"). 멀티데이 WAC 실현손익은 다른 계산 정의이며, **외부(portfolio-hub) WAC API 소비**라는 신규 연동이다.
- trex-core에 portfolio-hub `/pnl/realized` 클라이언트 메서드/포트가 없다(신규).
# 3. 문제점의 요약
- 현재 상태
- `operator_report.py`(REQ-088): `realized_pnl(trades)`가 `ledger_outbox` 당일 거래로 net(매도대금−매수대금−수수료−세금)을 계산. 자산별 분해 포함. `trex-core report` CLI로 노출.
- 이 정의는 **당일 매수→당일 매도(일중)** 에서만 실현손익과 일치한다. **전일 이전 매수분을 오늘 매도**하면, 당일 net은 "오늘 매도대금 − (오늘 없는 매수)"가 되어 cost basis가 빠진 채 실현손익이 과대/왜곡된다(중장기 보유의 본질).
- portfolio-hub `/pnl/realized`(REQ-040)는 매수 누적 WAC를 cost basis로 써 매도별 정확 실현손익을 주지만, trex-core 리포트가 이를 소비하지 않는다.
- 기대 상태
- 중장기 인스턴스 운영 리포트의 실현 PnL이 **portfolio-hub WAC API(/pnl/realized) 기준의 멀티데이 정확 실현손익**으로 표시된다(매수 cost basis 반영).
- portfolio-hub 이력 불완전 시(`data_completeness_warning`)가 리포트에 표면화된다.
- portfolio-hub 미가용 시 리포트가 안전하게 degrade(에러 표시 또는 당일 net 폴백)한다.
- 이해관계자
- 사장님 — 리포트 정의(멀티데이 기준)·범위 결정
- 팀장(Claude) — Discovery/게이트
- trex-core-dev — delivery
- 운영자 — 중장기 인스턴스 손익 리포트 소비자
- 제약사항
- portfolio-hub `/pnl/realized`는 **매도 기준 date-filter, 전 매수 WAC 참여**(REQ-041): 즉 기간 내 *매도*의 실현손익을 주고, 그 cost basis는 기간 밖 매수도 포함. (멀티데이 정의에 부합.)
- **매수만 한 날**은 매도가 없어 realized=0 — 사장님 정의("매수만 한 경우 수수료 비용")를 충족하려면 리포트가 **수수료(비용)를 별도 표시**해야 함(realized와 분리).
- trex-core wire 계약(slots RPC) 무관(리포트는 read-only 운영 표면). portfolio-hub HTTP 호출 추가(read).
## 주요 사용자 시나리오
- 운영자: 중장기 인스턴스의 `trex-core report`(또는 동등)로 기간 실현손익을 본다 → 전일 매수분을 오늘 매도한 손익이 WAC 기준으로 정확히 표시(당일 net 왜곡 아님).
- 운영자: 매수만 한 날 → 실현손익 0 + 수수료 비용 표시. 매도한 날 → WAC 실현손익. 둘 다 → 매수(WAC) 반영 후 매도 실현손익.
# 4. 이번에 정의할 변경
- 변경 1 — portfolio-hub `/pnl/realized` 소비 포트/어댑터 신규: `PortfolioHubReadPort`(또는 신규 포트)에 realized-pnl read 메서드 + HTTP 어댑터(`GET /v1/portfolios/{id}/pnl/realized`, from/to/symbol).
- 변경 2 — 운영 리포트의 실현 PnL을 WAC 기반으로 **전 인스턴스 통일**(OQ-1 결정): `operator_report.py`가 당일 net 대신 portfolio-hub WAC 실현손익을 사용(단·중·장기 공통 — WAC는 일중에도 정확). data_completeness_warning 표면화. 매수만 한 날의 수수료 비용 별도 표시.
- 변경 3 — degrade: portfolio-hub 미가용 시 **실현손익 섹션에 에러 명시**(OQ-5 결정 — 당일 net 폴백 안 함). 나머지 리포트 섹션(보유/체결/대기주문)은 정상 출력.
## 영향 범위
- 코드 영향: `trex_core/adapters/portfolio/*`(realized-pnl read 어댑터 신규), `trex_core/application/portfolio.py`(포트), `trex_core/application/operator_report.py`(실현 PnL 소스 분기), `trex_core/runtime/cli.py`(report 명령 — 기간/모드 옵션 필요시).
- 문서 영향: `docs/discovery/index.md`, srs/batches. `docs/operations.md`(리포트 해석 갱신).
- 운영 영향: 리포트 실현손익 정의가 중장기 모드에서 WAC로 바뀜 — 런북에 명시. portfolio-hub 호출 의존 추가.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: trex-core가 portfolio-hub `GET /v1/portfolios/{id}/pnl/realized`(from/to/symbol)를 호출하는 read 포트+어댑터를 갖는다.
- FR-2: 운영 리포트의 실현 PnL을 **전 인스턴스 공통으로** portfolio-hub WAC 실현손익 기준으로 산출·표시한다(당일 net 대체, 멀티데이 cost basis 반영). 자산별 분해 유지.
- FR-3: 매수만 한 기간(매도 0)은 실현손익 0 + **수수료 비용을 별도 항목으로 표시**(사장님 정의).
- FR-4: portfolio-hub `data_completeness_warning`(이력 불완전)을 리포트에 표면화한다.
- FR-5: portfolio-hub 미가용 시 **실현손익 섹션에 에러를 명시**하고(폴백 없음), 나머지 리포트 섹션은 정상 출력한다.
## 비기능 요구사항
- NFR-1: trex-core wire 계약(slots RPC) 불변 — 리포트는 read-only 운영 표면.
- NFR-2: 전 인스턴스 통일이므로, **일중 매수+매도(같은 날) 케이스에서 WAC 실현손익이 기존 당일 net과 일치**함을 회귀 테스트로 보장한다(단기 라이브 의미 보존 — WAC(=당일 매수가)·매도가 동일 정의).
- NFR-3: portfolio-hub 호출 실패가 데몬 본업(슬롯/집행)에 영향 주지 않는다(리포트 격리).
## 범위 경계
- In Scope
- portfolio-hub /pnl/realized 소비(포트+어댑터) + 중장기 운영 리포트 WAC 실현손익 + 매수일 수수료 표시 + completeness 경고 + degrade.
- Out of Scope
- portfolio-hub 측 변경(이미 REQ-040 제공 — 무변경).
- 미실현 손익(현재가 결합), 세무 리포트.
- 슬롯↔계좌 동기화(dcy-034 별건).
# 6. 리스크/가정 목록
## 리스크
- R-1 (Medium): WAC 실현손익 소스 전환이 단기 인스턴스 리포트(당일 net)에 회귀를 줄 수 있다.
- 완화: 모드/인스턴스로 분리(단기=net 유지 or WAC 통일은 OQ-1), 회귀 테스트.
- R-2 (Low): portfolio-hub 호출 추가로 리포트가 외부 의존·지연. 미가용 시 리포트 실패.
- 완화: degrade(폴백/에러 명시, OQ-5) + 본업 격리(NFR-3).
## 가정
- A-1: portfolio-hub `/pnl/realized`(REQ-040/041)는 멀티데이 WAC 실현손익을 정확히 제공한다(매도 date-filter + 전 매수 WAC).
- A-2: 중장기 인스턴스는 portfolio_id별로 실현손익을 조회한다(슬롯↔portfolio_id 1:1).
- A-3: trex-core는 portfolio-hub를 세션/토큰으로 호출 가능하다(기존 whoami/get_portfolios 경로 재사용).
## 오픈 질문
- OQ-1: WAC 실현손익을 전 인스턴스 통일인지, 중장기 모드만인지.
- 상태: **Answered (2026-06-26, 사장님)** — **전 인스턴스 WAC 통일**(당일 net 폐기). WAC는 일중에도 정확하므로 단기 라이브도 WAC로 통일하되, NFR-2 회귀(일중 케이스 net 일치)로 의미 보존.
- OQ-2: 리포트 기간 정의 — 당일/지정 기간(from~to). /pnl/realized는 from_dt/to_dt(매도 기준). 멀티데이 리포트의 기본 기간·옵션.
- 상태: Open / 처리: design, 팀장 / 종료: 기간 모델 확정 시 Answered
- OQ-3: 매수만 한 날 "수수료 비용" 표시 방식 — 실현손익과 별도 라인(비용)으로. 수수료 출처(portfolio-hub 거래내역 REQ-039 또는 trex-core ledger).
- 상태: Open / 처리: design, 팀장 / 종료: 표시·출처 확정 시 Answered
- OQ-4: trex-core가 realized PnL을 어떻게 조회 — portfolio_id별 N콜 vs daemon 집계. (slots↔portfolio 1:1이라 N콜이 자연스러우나 종목 多면 비용.)
- 상태: Open / 처리: design, 팀장 / 종료: 조회 단위 확정 시 Answered
- OQ-5: portfolio-hub 미가용 시 degrade.
- 상태: **Answered (2026-06-26, 사장님)** — **(b) 실현손익 섹션 에러 명시**(당일 net 폴백 없음). 나머지 섹션(보유/체결/대기주문) 정상 출력.
### 오픈 질문 처리 기준
- `Open`/`Answered`/`Deferred`/`Dropped` 표준 처리.
# 7. 초기 성공 기준
- S-1: 전일 이전 매수분을 당일 매도한 시나리오에서 리포트 실현손익이 **WAC 기준 정확값**(당일 net 왜곡 아님)으로 표시된다.
- S-2: 매수만 한 기간 → 실현손익 0 + 수수료 비용 표시.
- S-3: portfolio-hub `data_completeness_warning`이 리포트에 표면화된다.
- S-4: portfolio-hub 미가용 시 리포트가 degrade(정의된 방식)하고 데몬 본업 무영향.
- S-5: 일중 매수+매도(같은 날) 케이스에서 WAC 실현손익 = 기존 당일 net (단기 의미 보존, NFR-2).
## 측정 방식 및 데이터 출처
- S-1/S-2: 멀티데이 픽스처(전일 매수+당일 매도, 매수만) 통합 테스트(portfolio-hub 모킹) / 신규 테스트.
- S-3/S-4: 경고 주입·미가용 모킹 테스트.
- S-5: 기존 operator_report 테스트 회귀.
# 8. REQ로 넘기기 전 확인 체크
- [x] 적용 범위 확정 — OQ-1 = 전 인스턴스 WAC 통일
- [x] degrade 정책 — OQ-5 = 실현손익 섹션 에러 명시
- [ ] 기간/수수료/조회단위 design 인계 — OQ-2/3/4 (draft-req/design 단계)
- [x] 성공지표 측정 이견 없음
- [x] Freeze 담당자(CONFIRMED_BY) 지정 — 팀장
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-035_20260626_multiday-realized-pnl-report-wac.md
- 참조 문서:
- docs/discovery/dcy-029(운영 리포트 REQ-088), dcy-032(cost basis 오염)
- portfolio-hub dcy-007/REQ-040/041(WAC 실현 PnL), app/api/v1_portfolios.py:712, app/domain/pnl.py
- trex_core/application/operator_report.py(현행 당일 net), runtime/cli.py(report)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- (OQ-1) WAC 실현손익 적용 범위: 전 인스턴스 통일 vs 중장기 모드만.
- (OQ-5) portfolio-hub 미가용 시 degrade 방식.
## CONFIRM
- 본 건을 M3 중장기 운영 리포트(멀티데이 WAC 실현손익) 단위로 채택 — portfolio-hub 무변경, trex-core 연동.
## DATA
- (OQ-3 보조) 매수일 수수료 출처(portfolio-hub 거래내역 REQ-039 vs trex-core ledger) — 팀장 design서 확인.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5 > 범위 경계](#5-요구사항-목록) / 리스크·OQ: [# 6](#6-리스크가정-목록) / 성공: [# 7](#7-초기-성공-기준) / 인계 전 확인: [# 8](#8-req로-넘기기-전-확인-체크) / 결정: [# 10](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 진행 (인계) — OQ-1/OQ-5 사장님 결정 완료, OQ-2/3/4는 draft-req/design 단계 처리
- Handoff Rationale: portfolio-hub WAC API는 이미 존재(무변경), trex-core 연동만. 적용범위=전 인스턴스 WAC 통일, degrade=섹션 에러 명시 확정. 기간/수수료/조회단위는 REQ·design에서 구체화.
- Ready for REQ Drafting: true
- Confirmed By: 팀장 (Claude)
- Confirmed At (KST): 2026-06-26
# 12. 작성 가이드
- trex-core dcy 템플릿 기반. Open Questions는 가정 대체 금지. confirm-discovery 시 OQ 처리 + Freeze 갱신.
@@ -1,190 +0,0 @@
# 0. 문서 상태
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
- 상태: confirmed
- 문서 ID: dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load
- 이슈명: 그날-시작 KIS 인증정보 fetch(portfolio-hub) + fail-safe 거래 로드 — 진실원천 부재 시 신규 거래 미개시, 장중 무중단 (교차 발원 portfolio-hub dcy-010)
- 작성 시각(KST): 2026-06-28
- 마지막 갱신 시각(KST): 2026-07-14 (KST, dcy-037 HOTFIX follow-up link)
- 대체됨: 없음
- 후속 Discovery 참조: `docs/discovery/dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix.md` (fetched credential operator broker truth/token lifecycle 운영 환류)
- 생성된 REQ 참조: REQ-112, REQ-113, REQ-114, REQ-115
# 1. 계획 상태 요약
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: M3 메인(중장기 trex-core 인스턴스). 교차 발원 = portfolio-hub `dcy-010`(daemon-scoped 브로커 인증정보 소유·전달). 선행 = trex-core `dcy-027`(중장기 슬롯 운영 프로파일), `dcy-034`(cold-start reconcile)
- 해석:
- portfolio-hub가 universe·전략·KIS 인증정보의 단일 진실원천이라는 결정(사장님)에 따라, trex-core가 KIS 인증정보를 env 직접주입 대신 **그날 시작 시점에 portfolio-hub `/v1/user_info`(daemon-scoped 단일 broker)에서 fetch**해 `KISConfig`를 구성하도록 소비자측을 정의한다.
- 핵심 판단: "그날-시작 로드를 신선도 게이트로 삼아 진실원천 부재 시 신규 거래를 개시하지 않되(fail-closed), 이미 시작된 그날의 전략 실행은 장중 portfolio-hub 장애에도 중단하지 않는다"는 fail-safe 의미론을 trex-core 런타임에 어떻게 구현·경계지을지.
# 2. 요구사항 판정 결과
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
- 판정: 신규
- 근거
- 기존 trex-core는 KIS를 `TREX_KIS_BROKER_*` env로만 주입하며(`runtime/config.py`), portfolio-hub에서 인증정보를 받지 않는다. portfolio-hub 연동은 원장 쓰기(outbox)와 read(whoami/portfolios)만 존재.
- "그날-시작 fail-safe 로드 게이트"는 기존 어느 REQ도 정의한 바 없는 신규 런타임 동작이며, 외부 서비스(portfolio-hub) 의존을 매매 임계 경로 경계에 도입하는 구조 변경이라 독립 주기로 추적한다.
# 3. 문제점의 요약
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다.
- 현재 상태
- `KISConfig`는 `TREX_BROKER=kis`일 때 `TREX_KIS_BROKER_BASE_URL/APP_KEY/APP_SECRET/ACCOUNT_REF` 등 env에서만 구성된다(`trex_core/runtime/config.py:257`). 인스턴스별 계좌 분리는 프로세스별 env/.env 격리에만 의존.
- portfolio-hub read 어댑터(`trex_core/adapters/portfolio/hub_read.py`)가 이미 존재 — `whoami()`가 `GET /v1/user_info`를 호출해 `daemon_id`만 읽고, `get_portfolios(daemon_id)`로 universe(클라이언트측 daemon 필터)를 받는다. 인증 토큰(`TREX_PORTFOLIO_HUB_*`) 토대 존재.
- 일일 스냅샷 리프레시(`TREX_SNAPSHOT_SCHEDULE_KST`, 기본 07:30 KST; `SnapshotScheduleConfig`)가 존재 — "그날-시작" 로드의 자연스러운 후크 후보.
- 체결 원장은 trex-core 자기 PG `ledger_outbox`(REQ-019)에 즉시 durable 기록 후 portfolio-hub로 eventual drain → portfolio-hub 장애 시에도 거래내역 무손실(지연만).
- 기대 상태
- trex-core 인스턴스가 그날 시작 시 portfolio-hub에서 자기 daemon의 KIS 인증정보(+universe+전략+trading_enabled)를 받아 `KISConfig`를 구성하고 그날 거래를 개시한다.
- portfolio-hub가 그날-시작 시점에 부재/불가면 그날 신규 거래를 **개시하지 않는다(fail-closed)**.
- 장중 portfolio-hub 장애는 **이미 시작된 그날의 전략 실행(진입·청산 포함)을 중단시키지 않는다**. 그날 인증정보는 캐시로 운행, 원장은 outbox로 지연 흡수.
- 이해관계자
- 사장님(fail-safe 정책·계좌 분리 방향·결재), 팀장(Discovery·거버넌스), trex-core-dev(소비자 어댑터·로드 게이트 구현), 운영자(그날-시작 로드 실패 대응)
- 제약사항
- 라이브 단기 인스턴스(M1/M2)에 회귀 금지 — portfolio-hub 의존 도입이 기존 매매를 깨지 않아야 한다(break-glass 경로 필요).
- portfolio-hub dcy-010(daemon-scoped 단일 broker `/v1/user_info`)이 선행 의존 — 미배포 시 이 기능 비활성.
- 최민감 시크릿(app_secret)을 메모리/캐시에서 다루므로 로깅·노출 주의.
## 주요 사용자 시나리오
- 아침 07:30 스냅샷에서 중장기 인스턴스가 portfolio-hub fetch에 성공 → 자기 계좌로 그날 거래 개시. 11시 portfolio-hub가 잠시 다운되어도 그날 진입/청산은 캐시된 인증정보로 계속, 원장은 복구 후 drain.
- 아침 07:30에 portfolio-hub가 다운 → 인스턴스는 그날 신규 거래를 개시하지 않고 운영자에게 알람. portfolio-hub 복구 후 다음 로드 사이클에 정상화.
- 운영자가 비상 시 env로 KIS 인증정보를 직접 주입(break-glass)해 portfolio-hub 없이 인스턴스를 띄운다.
# 4. 이번에 정의할 변경
이번 Discovery에서 다루는 핵심 변경 항목.
- (C1) **KISConfig 소스 확장** — env-only → "그날-시작 portfolio-hub fetch"를 1차 소스로, env를 break-glass 오버라이드로. (`hub_read` 어댑터에 broker 인증정보 read 추가)
- (C2) **그날-시작 로드 게이트(fail-closed 진입)** — 로드 시점 portfolio-hub 부재 시 그날 신규 거래(진입) 미개시. 로드 성공분으로 그날 운행.
- (C3) **장중 무중단 보장** — 그날 시작 후 portfolio-hub 장애가 이미 로드된 그날 실행(진입·청산)을 중단시키지 않음(인증정보 캐시 + 원장 outbox 의존).
## 영향 범위
- 코드 영향:
- `trex_core/adapters/portfolio/hub_read.py`(broker 인증정보 read 추가), `trex_core/runtime/config.py`(KISConfig 소스/우선순위), 그날-시작 로드 후크(`application/snapshot_scheduler.py` 또는 daemon 부트스트랩), 로드 게이트가 진입 admission(IntentService)와 맞물리는 지점
- 캐시 저장소(기존 `TREX_KIS_REDIS_URL` 토큰 캐시 패턴 재사용 검토)
- 문서 영향:
- `docs/srs/`(신규 REQ), `docs/discovery/index.md`(본 문서 등록), `docs/runtime-flows.md`(그날-시작 로드 흐름 추가 검토)
- 교차: portfolio-hub `dcy-010` 계약 소비측 정합
- 운영 영향:
- 그날-시작 로드 실패 시 운영 절차(알람·수동 개입·break-glass). 인스턴스별 service token(daemon_id 바인딩) 발급·배치.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: trex-core가 그날-시작 시점에 service token으로 portfolio-hub `/v1/user_info`(daemon-scoped 단일 broker)를 호출해 KIS 인증정보(account_ref + app_key/app_secret + credential_version)를 받아 `KISConfig`를 구성한다. **그날-시작 체크포인트는 horizon별로 분리(결정 2026-06-28)**: 단기 = 07:30 KST 스냅샷 1회, 중장기 = dcy-027 운영 프로파일 기반 리밸런싱 주기. 그날 로드된 인증정보는 **그날 고정**이며 장중 portfolio-hub 복구 시에도 재fetch하지 않는다(결정론).
- FR-2: 그날-시작 로드 시점에 portfolio-hub가 불가(네트워크/비2xx/인증정보 부재/`trading_enabled=false`)면 그날 **신규 진입을 개시하지 않는다(fail-closed)**. 사유를 로깅하고 운영자에게 알람한다.
- FR-3: 그날 시작 후 발생한 portfolio-hub 장애는 이미 로드된 그날 실행을 중단시키지 않는다. 그날 인증정보는 캐시로 유지되어 진입·청산이 계속된다.
- FR-4: 평시에는 **portfolio-hub fetch가 1차 소스**이고, env(`TREX_KIS_BROKER_*`)는 **명시적 break-glass 오버라이드**다(결정 2026-06-28). 즉 fetch를 우선 시도하되, 운영자가 의도적으로 env break-glass를 활성화하면 portfolio-hub 없이도 인스턴스를 띄울 수 있다. 단기 인스턴스(M1/M2)는 회귀 차단을 위해 **당분간 fetch opt-in**(미설정 시 기존 env-only 경로 보존, NFR-1).
## 비기능 요구사항
- NFR-1 (회귀안전): portfolio-hub fetch 미설정/비활성 시 기존 env-only 동작이 byte-for-byte 보존된다(M1/M2 단기 인스턴스 무회귀).
- NFR-2 (보안): 캐시·로그에 app_secret 평문이 남지 않는다(마스킹). 전송은 내부망/TLS·service-token.
- NFR-3 (관측성): 그날-시작 로드 성공/실패/캐시운행 상태가 로그·카운터·알람으로 가시화된다.
## 범위 경계
- In Scope
- trex-core 소비자 어댑터(인증정보 read) + 그날-시작 로드 게이트(fail-closed 진입) + 장중 무중단 + break-glass.
- Out of Scope
- portfolio-hub 측 daemon-scoped 인증정보 소유·전달(= dcy-010, 선행 의존).
- universe·전략 fetch 자체의 재설계(현 get_portfolios 경로 재사용; 본 문서는 인증정보 로드와 fail-safe 게이트에 집중).
- {{DECIDE_BOOTSTRAP_UNIFY:universe+인증정보+trading_enabled를 단일 부트스트랩 호출로 통합할지 — dcy-010 OQ-3와 공동 결정}}
# 6. 리스크/가정 목록
## 리스크
- R-1 (High): portfolio-hub 의존 도입이 라이브 단기 인스턴스(M1/M2) 매매를 깨뜨림(회귀).
- 완화 방안: fetch는 opt-in. 미설정 시 env-only 경로 보존(NFR-1). break-glass 오버라이드 상시 유지. 단기 인스턴스는 당분간 env 유지 가능.
- R-2 (High): "그날-시작"의 정의가 horizon별로 달라(단기=매일 07:30 / 중장기=리밸런싱 주기) 로드 게이트 의미가 모호.
- 완화 방안: OQ-1로 horizon별 체크포인트 정의를 confirm 전 확정. 단기는 07:30 스냅샷, 중장기는 프로파일 기반 주기로 분리.
- R-3 (Medium): app_secret을 메모리/캐시에서 다뤄 노출 표면 증가.
- 완화 방안: 마스킹 로깅, 캐시 TTL/스코프 한정, Redis 사용 시 키 스코프(daemon/앱키) 격리.
## 가정
- A-1: portfolio-hub = universe·전략·KIS 인증정보의 단일 진실원천. (사장님 confirmed)
- A-2: 신선도 게이트 = **그날-시작 로드 시점**. 장중 portfolio-hub 장애는 그날 실행을 중단시키지 않는다("그날의 전략 실행은 그대로 유지"). (사장님 confirmed 2026-06-28)
- A-3: portfolio-hub `dcy-010`(daemon-scoped 단일 broker `/v1/user_info`)이 선행 제공된다.
- A-4: env break-glass 오버라이드는 유지된다(비상 경로).
- A-5: 그날 거래내역은 trex-core `ledger_outbox`로 무손실 흡수되므로 장중 portfolio-hub 장애가 원장 유실로 이어지지 않는다.
## 오픈 질문
- OQ-1: "그날-시작" 로드 체크포인트 정의 (horizon별).
- 상태: **Answered (2026-06-28)**
- 결정: 단기 = 07:30 KST 스냅샷 1회, 중장기 = dcy-027 운영 프로파일 기반 리밸런싱 주기로 분리. 본문 FR-1에 반영.
- 반영 위치: §5 FR-1.
- OQ-2: 장중 portfolio-hub 복구 시 재fetch vs 그날 고정.
- 상태: **Answered (2026-06-28)**
- 결정: **그날 고정(결정론)** — 장중 재fetch 안 함. 본문 FR-1에 반영.
- 반영 위치: §5 FR-1.
- OQ-3: break-glass(env)와 fetch 우선순위.
- 상태: **Answered (2026-06-28)**
- 결정: **fetch 1차, env는 명시적 break-glass 오버라이드**(평시 fetch 우선). 단기는 fetch opt-in으로 회귀 차단. 본문 FR-4에 반영.
- 반영 위치: §5 FR-4, NFR-1.
- OQ-4: 그날-시작 로드 실패 알람 경로 — trex-notifier(dcy-033) 연동 형태와 등급(CRITICAL?).
- 상태: Deferred
- 처리 방안: dcy-033 알람 envelope 계약 재사용. REQ 단계에서 구체화.
- 종료 조건: REQ 작성 시 알람 등급·채널 확정.
# 7. 초기 성공 기준
- S-1: portfolio-hub fetch 성공 시 그날 거래가 fetch된 계좌 인증정보로 개시됨을 검증할 수 있다.
- S-2: 그날-시작 시점 portfolio-hub 불가 시 신규 진입이 개시되지 않고(차단) 알람이 발생함이 테스트로 확인된다.
- S-3: 그날 시작 후 portfolio-hub를 내려도 그날 진입/청산이 캐시 인증정보로 계속되고 원장이 복구 후 drain됨이 검증된다.
- S-4: fetch 미설정 시 기존 env-only 동작이 회귀 없이 보존됨이 테스트로 확인된다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: fetch 모의 + KISConfig 구성 결과 검증 / 데이터 출처: 통합테스트
- S-2 측정 방식: 로드 시점 portfolio-hub 불가 주입 → 진입 admission 차단 + 알람 카운터 / 데이터 출처: 통합테스트, 로그/카운터
- S-3 측정 방식: 그날 시작 후 portfolio-hub 다운 주입 → 청산 경로 동작 + outbox drain 확인 / 데이터 출처: 통합테스트, ledger_outbox 행
- S-4 측정 방식: fetch 미설정 환경에서 기존 테스트 그린 / 데이터 출처: 기존 회귀 스위트
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 기준: 부트스트랩 통합(dcy-010 OQ-3 공동)은 Out of Scope Deferred 처리
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 기준: S-1~S-4 측정 가능성 검토 완료
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 기준: R-1(회귀 — fetch opt-in·break-glass, trex-core-dev)·R-2(체크포인트 horizon별 정의 확정) 완화안 확정
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 기준: OQ-1·OQ-2·OQ-3 Answered, OQ-4 Deferred 처리 완료
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 기준: 사장님+팀장 confirm-discovery (DECIDE/CONFIRM 전건 해소)
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: docs/discovery/dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load.md
- 참조 문서:
- portfolio-hub docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md (교차 발원 — 인증정보 소유·전달)
- docs/discovery/dcy-027_20260612_long-term-strategy-slot-operation.md (중장기 운영 프로파일)
- docs/discovery/dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart.md (cold-start 동기화)
- trex_core/adapters/portfolio/hub_read.py / trex_core/runtime/config.py (현재 구현)
# 10. 사용자 결정 필요 항목 요약
## DECIDE
- ~~그날-시작 체크포인트 (OQ-1)~~ → **RESOLVED 2026-06-28: 단기 07:30 스냅샷 / 중장기 dcy-027 프로파일 주기**
- ~~장중 재fetch (OQ-2)~~ → **RESOLVED 2026-06-28: 그날 고정(결정론)**
- ~~break-glass 우선순위 (OQ-3)~~ → **RESOLVED 2026-06-28: fetch 1차, env 명시적 break-glass**
## CONFIRM
- ~~fail-safe 모델 (A-2)~~ → **RESOLVED: 그날-시작 fail-closed 진입 + 장중 무중단 confirmed (사장님)**
- ~~단기 fetch opt-in~~ → **RESOLVED 2026-06-28: 단기 인스턴스 당분간 env 유지(fetch opt-in)로 회귀 차단 (FR-4/NFR-1)**
## DATA
- {{DATA_SERVICE_TOKEN_PROVISION:인스턴스별 service token(daemon_id 바인딩) 발급·배치 경로 — REQ/ops 단계에서 구체화(Deferred)}}
# 11. Discovery Freeze
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다.
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: REQ Drafting 진행 가능
- Handoff Rationale: confirm-discovery 완료(2026-06-28). DECIDE 3건(OQ-1 horizon별 체크포인트·OQ-2 그날 고정·OQ-3 fetch 1차+env break-glass) 전건 Answered, CONFIRM 2건(fail-safe 모델·단기 opt-in) 해소, OQ-4(알람) Deferred. 잔여 Open 0건. **단, REQ 인계·구현은 선행 의존 portfolio-hub dcy-010(daemon-scoped 단일 broker /v1/user_info) 진척에 맞춰 조율.**
- Ready for REQ Drafting: true
- Confirmed By: 팀장(Claude)
- Confirmed At (KST): 2026-06-28
@@ -1,197 +0,0 @@
# 0. 문서 상태
- 상태: confirmed
- 문서 ID: dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix
- 이슈명: HOTFIX — fetched credential 운영자 broker truth 정합 및 managed non-holding divergence 분리 (#39, #40, #41)
- 작성 시각(KST): 2026-07-14 21:52:01 KST
- 마지막 갱신 시각(KST): 2026-07-14 22:24:41 KST (draft-req)
- 대체됨: 없음
- 후속 Discovery 참조: `dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding` — portfolio-hub cost-basis canonical response parsing 및 안전한 sync avg_price seed P1 follow-up
- 생성된 REQ 참조: REQ-119, REQ-120, REQ-121, REQ-122
# 1. 계획 상태 요약
- 연계 문서 현황:
- 관련 점수 파일: 미생성
- 관련 TODO/메모: GitHub `fourblocks2020/trex-core` issue [#39](https://github.com/fourblocks2020/trex-core/issues/39), [#40](https://github.com/fourblocks2020/trex-core/issues/40), [#41](https://github.com/fourblocks2020/trex-core/issues/41). 선행 Discovery `dcy-034`(차단형 reconcile/sync/verify) 및 `dcy-036`(fetched credential day-start fail-safe).
- 해석:
- `credential_fetch_enabled=true`인 `genichin-longterm`에서 daemon, operator CLI, broker token lifecycle이 같은 계좌 잔고 truth를 보장하지 못하여 `sync`/`verify` 결과를 운영 복구 판단에 사용할 수 없다.
- 이 HOTFIX는 잘못된 broker truth를 빈 잔고로 해석하는 경로를 차단하고, 이미 관리되는 inactive/error 슬롯을 진짜 unmanaged position과 구분해 안전한 운영자 triage와 이후 sync를 가능하게 한다.
# 2. 요구사항 판정 결과
- 판정: 신규
- 근거
- `dcy-034`/REQ-105·106은 `sync`/`verify`의 상태 동기화 의미론을 정의했지만, fetched credential 모드에서 CLI가 daemon과 동일한 credential source 및 token lifecycle을 사용해야 한다는 운영 불변식은 정의하지 않았다.
- `dcy-036`/REQ-113·115는 day-start fetched credential과 fail-safe entry gate를 다루지만, direct-DB operator CLI의 credential 재해석·token throttle 및 inactive 슬롯의 reconcile 분류는 후속 운영 환류로 독립 추적하는 편이 명확하다.
# 3. 문제점의 요약
- 현재 상태
- issue #39 재현에서 daemon은 portfolio-hub fetched credential로 계좌 A를 사용하지만, `sync`/`verify`는 `_build_broker(cfg)`로 env/config 계좌 B를 생성했다. 동일 instance의 `005930`은 daemon broker truth에서 qty=5이나 CLI dry-run은 `broker_qty_zero`로 skip했다. 계좌 식별자는 문서·로그에서 안전하게 마스킹한다.
- issue #40의 `KISBroker.get_balance()`는 token/auth HTTP 오류를 잡아 `[]`로 반환한다. fresh fetched-credential broker가 KIS token throttle(`EGW00133`)을 받으면 실제 보유가 없는 것처럼 보이며, `broker_qty_zero` 같은 상태 판단으로 이어질 수 있다.
- issue #41의 `reconcile_core_holdings()`는 `list_holding_slots(instance_key)`만 대조한다. 따라서 broker가 보유한 symbol에 대해 같은 instance의 managed slot이 `inactive` 또는 `error`로 존재해도 `unmanaged position`으로 로그를 남긴다.
- 기대 상태
- fetched credential 모드에서 daemon과 `sync`/`verify`는 동일한 portfolio-hub credential source와 명시적인 token/broker lifecycle 정책으로 broker truth를 조회하며, 인증·token 실패는 empty balance와 절대 동치가 아니다.
- reconciliation은 `slot 없음`의 true unmanaged position과 `slot 존재 + non-holding`의 managed state divergence를 별도 code/log/운영 조치로 구분한다.
- 이해관계자
- 사장님: hotfix 범위·production 배포 및 실계좌 `sync --live` 최종 승인자
- 팀장: Discovery/REQ/Batch 및 release gate 관리
- trex-core-dev: broker 구성, reconciliation, CLI 및 회귀 테스트 delivery 담당
- qc-reviewer: credential source, throttle fail-closed, divergence 분류의 독립 검증 담당
- 운영자: dry-run 검증 후 승인된 live sync를 수행하는 주체
- 제약사항
- app key/app secret/token 및 raw credential payload는 로그, JSON 출력, 테스트 fixture에 기록하지 않는다.
- `sync --live`는 외부 실계좌/DB 상태를 변경하므로 본 HOTFIX 배포 후에도 별도 운영 승인 없이는 실행하지 않는다.
- 기존 `credential_fetch_enabled=false` env-only instance와 AMQP wire 계약은 회귀 없이 유지한다.
## 주요 사용자 시나리오
- 운영자가 `genichin-longterm`의 inactive slot과 broker-held `005930`을 확인한다. `sync --dry-run`은 fetched credential source로 broker qty=5를 확인하고, token 오류가 나면 0 보유로 판단하지 않고 비0 종료와 안전한 오류를 반환한다.
- daemon reconcile이 `inactive` managed slot + broker qty>0을 발견하면 `managed_non_holding_divergence`로 구조화해 경고한다. slot이 전혀 없는 broker-held symbol만 기존 `unmanaged_position` 경로를 탄다.
- hotfix QC와 release 검증 후 운영자가 dry-run 결과, portfolio-hub 장부, broker truth를 교차 확인하고 명시 승인된 단일 대상에만 `sync --live --yes`를 실행한다.
# 4. 이번에 정의할 변경
- 변경 1 — daemon/CLI 공통 broker truth 해석: `credential_fetch_enabled=true`일 때 operator CLI도 portfolio-hub fetched credential을 사용하고, raw env/config credential은 명시적인 break-glass 정책 외에는 조용히 fallback하지 않는다.
- 변경 2 — token/auth 실패 fail-loud: `get_balance()`와 operator 표면에서 token issuance/auth/transport 실패를 actual empty holdings와 분리하고, 반복 조회에 대한 broker/token 재사용 정책을 명확히 한다.
- 변경 3 — managed non-holding divergence 분류: holding-only 수량 대조는 보존하되, 미매칭 broker symbol에 대해 전체 slot 존재를 확인해 `unmanaged_position`과 `managed_non_holding_divergence`를 구분한다.
- 변경 4 — baseline/운영 문서 정합: credential source, error semantics, reconcile decision code, dry-run 및 live remediation gate를 `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/operations.md`에 실제 구현과 일치하게 반영한다.
## 영향 범위
- 코드 영향:
- `trex_core/runtime/daemon.py`, `trex_core/runtime/cli.py`, `trex_core/application/snapshot_scheduler.py` 또는 공통 broker factory: fetched credential source 및 CLI broker 생성 경로.
- `trex_core/adapters/broker/kis.py`: token/auth failure와 actual empty balance의 구분, KIS token lifecycle.
- `trex_core/application/reconciliation.py`, SlotRepository protocol 및 PostgreSQL/InMemory 구현: non-holding managed-slot existence check와 구조화 classification.
- 문서 영향:
- `docs/runtime-flows.md`, `docs/interface-contract.md`, `docs/operations.md`, 신규 REQ/Batch/verification 문서.
- `dcy-034`, `dcy-036`: 본 HOTFIX follow-up 관계 기록.
- 운영 영향:
- 배포 전 dry-run에서 credential source·account reference masking·broker query 결과·분류 code를 확인한다.
- 배포 후 live sync는 single portfolio/slot 대상, 사전 dry-run 및 사장님 승인으로 제한한다.
# 5. 요구사항 목록
## 기능 요구사항
- FR-1: `credential_fetch_enabled=true` instance의 daemon, `sync`, `verify` 및 해당 operator broker-read 경로는 동일한 portfolio-hub fetched credential source를 해석해야 한다. fetch 실패 시 명시적 break-glass가 활성화되지 않은 한 env/config credential로 조용히 fallback하지 않는다.
- FR-2: token issuance, auth, HTTP transport 또는 malformed broker response 실패는 `get_balance()==[]`로 변환하지 않는다. operator CLI는 안전하게 sanitize된 오류와 비0 종료로 중단하며, `broker_qty_zero`/sync skip/verify 결과를 만들지 않는다.
- FR-3: repeated fetched-credential broker read는 구현된 token/broker lifecycle 정책에 따라 안정적으로 동작해야 하며, fresh object 생성으로 인한 token throttle을 실제 빈 계좌로 오판하지 않아야 한다.
- FR-4: `reconcile_core_holdings()`는 broker-held symbol이 holding slot에 매칭되지 않을 때 (a) instance에 어떤 slot도 없는 `unmanaged_position`, (b) slot은 있으나 `inactive`/`error` 등 non-holding인 `managed_non_holding_divergence`를 구분해 구조화 로그/decision code를 남겨야 한다.
- FR-5: `managed_non_holding_divergence`는 자동 slot 전이, 자동 warning 해제, 자동 매매 또는 자동 청산을 유발하지 않는다. 상태 수렴은 검증된 `sync`와 기존 운영 gate를 통해서만 수행한다.
- FR-6: runtime/interface/operations baseline은 credential source 우선순위, safe error semantics, reconciliation 분류 및 operator remediation 절차를 구현과 동기화해야 한다.
## 비기능 요구사항
- NFR-1 (안전성): credential, access token, app secret, raw account number 및 HTTP authorization payload는 로그·CLI JSON·테스트 artifact에 노출되지 않는다. account reference가 필요한 운영 식별은 마스킹 또는 비밀이 아닌 stable label을 사용한다.
- NFR-2 (회귀안전): `credential_fetch_enabled=false` env-only instance, 정상 holding match, holding qty mismatch, slot 없는 unmanaged position, 기존 `sync`/`verify` live double-ack 의미론은 보존한다.
- NFR-3 (관측성): source mode, safe failure code, reconciliation decision code, slot_id/portfolio_id/symbol/status가 구조화 로그로 추적 가능해야 한다.
- NFR-4 (검증): unit/integration/CLI regression에 #39·#40·#41 재현 조건과 existing reconciliation/sync/verify 경로를 포함하고, independent QC가 실제 suite를 재실행한다.
## 범위 경계
- In Scope
- #39 fetched credential source mismatch, #40 token throttle/empty balance masking, #41 managed non-holding divergence 분류 및 이들의 tests/docs.
- operator dry-run evidence 및 post-release single-target remediation runbook.
- Out of Scope
- portfolio-hub credential API 계약 또는 credential 저장 모델의 재설계; 필요한 계약 변화가 발견되면 cross-service gate로 중단한다.
- `sync --live`의 자동 실행, 전역 bulk sync, unmanaged broker position 자동 slot 생성, automatic remediation/trading.
- TVR 전략 로직 변경 또는 TVR 재배포. TVR은 core truth/slot 상태가 정합된 뒤 기존 `inactive,holding` read 의미론으로 재평가한다.
# 6. 리스크/가정 목록
## 리스크
- R-1 (High): credential source 또는 account identity를 잘못 연결하면 다른 실계좌의 broker truth를 기준으로 sync 판단을 할 수 있다.
- 완화 방안: fetched mode CLI source를 daemon과 공통화하고, dry-run에서 비밀 없는 source/account fingerprint 일치 assertion을 테스트한다. live sync는 사전 dry-run과 명시적 승인 없이는 실행하지 않는다.
- R-2 (High): auth/token 오류를 empty balance로 취급하면 false zero 기반의 잘못된 상태 수렴이 발생할 수 있다.
- 완화 방안: typed/sanitized failure를 도입하고 balance가 성공적으로 조회된 경우에만 qty=0 판단을 허용한다. throttling 재현 테스트를 추가한다.
- R-3 (Medium): non-holding slot existence check가 holding 수량 mismatch 또는 true unmanaged warning 의미론을 바꿀 수 있다.
- 완화 방안: primary holding compare를 유지한 뒤 unmatched broker symbols에만 second-pass existence lookup을 적용하고, 4-case regression matrix를 고정한다.
- R-4 (Medium): HOTFIX 배포 직후 운영자가 빠르게 live sync를 실행하면서 장부/브로커/slot 증거 교차 확인을 건너뛸 수 있다.
- 완화 방안: release runbook에 dry-run → broker/ledger cross-check → single-target explicit approval → live sync → re-reconcile 순서를 필수 gate로 기록한다.
## 가정
- A-1: portfolio-hub fetched credential path는 daemon의 현재 정상 운행에서 authority source로 사용 가능하며, service-token 접근 계약은 변경하지 않는다.
- A-2: `sync`는 REQ-105의 기존 원칙대로 managed slot만 브로커 truth에 수렴시키며, slot 없는 broker holdings를 자동 편입하지 않는다.
- A-3: `genichin-longterm`의 `005930` 사례는 generic regression fixture로 추상화할 수 있고, 실제 계좌 식별값/credential은 test data에 포함하지 않는다.
## 오픈 질문
- OQ-1: #39, #40, #41을 하나의 emergency delivery batch로 묶어 single release candidate로 배포할지, credential hotfix(#39/#40)와 telemetry 분류(#41)를 분리할지?
- 상태: Answered (2026-07-14, 사장님)
- 결정: #39·#40·#41을 하나의 emergency batch로 확정한다. #41은 자동 state write가 없는 additive classification으로 유지해 rollback blast radius를 제한한다.
- 종료 근거: 사장님이 2026-07-14에 단일 emergency batch를 명시 승인했다.
- OQ-2: operator CLI의 fetched credential broker/token lifecycle을 process-local reuse, shared token cache, 또는 다른 공통 provider 중 무엇으로 고정할지?
- 상태: Deferred to REQ
- 처리 방안: delivery-runner/trex-core-dev가 current KISBroker token cache·daemon reinitialization·CLI process lifetime을 분석해 secret-safe alternatives와 retry/throttle behavior를 REQ draft에 제시한다.
- 인계 조건: REQ가 token lifecycle ownership, error type, retry/no-retry policy 및 regression test seam을 명시한다. 이는 구현 설계 선택이며 Discovery 범위·성공 기준을 바꾸지 않는다.
- OQ-3: hotfix release 뒤 `genichin-longterm`의 live sync 실행 대상을 `005930` 단일 slot으로 확정할지?
- 상태: Deferred to release gate
- 처리 방안: hotfix deployment와 dry-run 결과 후 사장님이 broker/ledger/slot cross-check evidence를 보고 승인한다.
- 인계 조건: release approval에 target portfolio/slot, executed command class, before/after evidence, rollback/hold plan이 기록된다. 본 Discovery는 live sync 실행을 승인하지 않는다.
# 7. 초기 성공 기준
- S-1: fetched credential enabled fixture에서 daemon-equivalent path와 `sync --dry-run`/`verify`가 동일한 masked source identity와 동일 broker holding set을 사용한다.
- S-2: KIS token throttle/auth failure fixture에서 CLI가 non-zero failure와 sanitized code를 반환하고, empty balance/`broker_qty_zero`/state write를 만들지 않는다.
- S-3: 4-case reconciliation regression이 통과한다: slot 없음→unmanaged, inactive slot+broker holding→managed_non_holding_divergence, matching holding→ok, holding qty mismatch→기존 mismatch path.
- S-4: 실제 deployed hotfix에서 approved dry-run이 `genichin-longterm` 대상 broker truth와 divergence classification을 확인하며, live sync 전에는 DB 상태가 변경되지 않는다.
## 측정 방식 및 데이터 출처
- S-1 측정 방식: daemon/CLI 공통 provider와 mocked fetched credential/broker fixture의 account fingerprint·balance 결과 비교 테스트 / 데이터 출처: 신규 runtime CLI 및 broker factory tests.
- S-2 측정 방식: token issuance error를 주입한 `get_balance`/CLI test에서 exit code, sanitized error, no-write assertion 확인 / 데이터 출처: KIS adapter 및 CLI tests.
- S-3 측정 방식: `reconcile_core_holdings` application tests와 PostgreSQL/InMemory repository parity tests 실행 / 데이터 출처: `tests/application/test_holding_divergence.py` 및 추가 regression tests.
- S-4 측정 방식: release runbook에 따른 production dry-run command output, broker/ledger/slot readback, audit logs 확인 / 데이터 출처: deployment evidence 및 운영자 기록. 실계좌 write는 승인 후 별도 증거로만 수행한다.
# 8. REQ로 넘기기 전 확인 체크
- [x] 범위(In Scope/Out of Scope) 확정
- 근거: 사장님이 2026-07-14 #39/#40/#41 단일 emergency batch를 승인했고, cross-service 계약 변경은 발견 시 별도 gate로 중단한다.
- [x] 성공 지표의 측정 방식/데이터 출처 확정
- 근거: S-1~S-4에 test seam, safe production dry-run evidence, independent QC 재실행 요구가 명시되었다. runtime 계측값은 REQ/release evidence로 수집한다.
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
- 근거: R-1/R-2 delivery owner=delivery-runner/trex-core-dev, independent verifier=qc-reviewer, live operation approver=사장님으로 고정한다.
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
- 근거: OQ-1 Answered, OQ-2는 REQ 설계로 Deferred, OQ-3은 release approval gate로 Deferred 상태다.
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
- 근거: 사장님(genichin)이 2026-07-14 HOTFIX scope를 confirm-discovery에서 승인했다. live sync 실행은 별도 release approval 대상이다.
# 9. 파일 처리 결과
- 처리 결과: 생성
- 생성 경로: `docs/discovery/dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix.md`
- 참조 문서:
- `docs/discovery/dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart.md`
- `docs/discovery/dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load.md`
- GitHub issues #39, #40, #41; `trex_core/runtime/daemon.py`, `trex_core/runtime/cli.py`, `trex_core/adapters/broker/kis.py`, `trex_core/application/reconciliation.py`
- `.stage-pilot/templates/runtime-flows.md`, `.stage-pilot/templates/project-structure.md`
# 10. 사용자 결정 필요 항목 요약
## 확정된 결정
- HOTFIX delivery boundary: #39·#40·#41을 하나의 emergency batch로 구현·검증해 single release candidate로 관리한다 (2026-07-14, 사장님 승인).
- Production safety gate: hotfix 배포 뒤 `sync --live`는 dry-run 및 broker/ledger/slot cross-check evidence가 준비된 경우에도 사장님 명시 release approval이 있어야만 실행한다.
- Post-deploy evidence: target instance의 masked credential source, broker holdings, slot 상태, portfolio-hub ledger readback은 REQ verification 및 release gate에서 수집한다. 현재는 구현 전이므로 실제 운영 데이터는 없다.
# 11. Discovery Freeze
## 섹션 참조
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
## Freeze 플래그
- Handoff Decision: 승인
- Handoff Rationale: 사장님이 #39/#40/#41 단일 emergency batch scope를 승인했다. OQ-2는 REQ에서 구현 설계로 구체화하며, OQ-3은 배포 후 별도 release approval gate이므로 REQ drafting을 막지 않는다.
- Ready for REQ Drafting: true
- Confirmed By: genichin
- Confirmed At (KST): 2026-07-14 22:19:29 KST
-48
View File
@@ -1,48 +0,0 @@
# Discovery 전체 인덱스
<!-- 아래 테이블은 /new-discovery 실행 시 자동 갱신된다. 수동 편집 금지. -->
## Discovery 목록
| ID | 날짜 | 이슈명 | 문서 | 상태 |
|---|---|---|---|---|
| dcy-001_20260603_slot-pure-domain-migration | 20260603 | trex-core 베이스 수립 및 slot 순수 도메인 이식 | [Discovery](./dcy-001_20260603_slot-pure-domain-migration.md) | confirmed |
| dcy-002_20260603_slot-core-daemon-http-api | 20260603 | trex-core 슬롯 코어 데몬화 + HTTP intent API | [Discovery](./dcy-002_20260603_slot-core-daemon-http-api.md) | confirmed |
| dcy-003_20260604_broker-execution-and-outbox | 20260604 | 브로커 단일 세션 + 주문 집행 + durable outbox (불변식 B) | [Discovery](./dcy-003_20260604_broker-execution-and-outbox.md) | confirmed |
| dcy-004_20260604_portfolio-hub-ledger-integration | 20260604 | portfolio-hub ledger 연동 — 체결을 외부 원장에 기록 | [Discovery](./dcy-004_20260604_portfolio-hub-ledger-integration.md) | confirmed |
| dcy-005_20260604_portfolio-hub-asset-snapshot-api | 20260604 | portfolio-hub 종목 스냅샷 read API — 전략 모듈에 전략 실행 대상 자산 리스트 제공 | [Discovery](./dcy-005_20260604_portfolio-hub-asset-snapshot-api.md) | confirmed |
| dcy-006_20260605_strategy-module-rabbitmq-transport | 20260605 | trex-core ↔ 전략 모듈 통신 transport — RabbitMQ 도입 (스냅샷 read + intent 제출 + 이벤트) | [Discovery](./dcy-006_20260605_strategy-module-rabbitmq-transport.md) | confirmed |
| dcy-007_20260606_core-autonomous-safety-sell | 20260606 | core 자율 안전 매도 — stop-loss·시간 만기 자동 트리거 | [Discovery](./dcy-007_20260606_core-autonomous-safety-sell.md) | confirmed |
| dcy-008_20260607_emergency-stop-kill-switch | 20260607 | 비상 정지/강제 청산(kill-switch) — portfolio/전역 슬롯 청산 + 당일 전략 제외 | [Discovery](./dcy-008_20260607_emergency-stop-kill-switch.md) | confirmed |
| dcy-009_20260607_multi-instance-rabbitmq-topology | 20260607 | 인스턴스별 독립 RabbitMQ 채널 토폴로지 — trex-core 다중 인스턴스 운영을 위한 Topic Exchange + Fanout Exchange per-instance 큐 분리 | [Discovery](./dcy-009_20260607_multi-instance-rabbitmq-topology.md) | confirmed |
| dcy-010_20260607_safety-sell-persistent-rejection-backoff | 20260607 | safety-sell 영속 거부 재시도 백오프 — 장 휴장·잔고 부족·규제 등 지속 거부 상황에서 KIS 주문 spam 방지를 위한 슬롯별 재시도 backoff/cooldown 정책 | [Discovery](./dcy-010_20260607_safety-sell-persistent-rejection-backoff.md) | confirmed |
| dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve | 20260608 | buy intent — portfolio_id를 필수 키로, symbol은 universe(SnapshotStore)에서 resolve (+ issue #11 portfolio_id 누락 체결 ledger silent skip 가시화 흡수) | [Discovery](./dcy-011_20260608_buy-intent-portfolio-id-required-symbol-resolve.md) | confirmed |
| dcy-012_20260608_sell-order-pricing-entry-price-bug | 20260608 | 매도 주문이 현재가가 아닌 entry_price(매수가) 지정가로 나가는 버그 — sell dispatch 가격 정책 부재 (issue #10) | [Discovery](./dcy-012_20260608_sell-order-pricing-entry-price-bug.md) | confirmed |
| dcy-013_20260608_kis-external-cancel-pending-sell-stuck | 20260608 | KIS 외부 주문 취소 시 슬롯이 pending_sell 에 영구히 갇힘 — cncl_yn 미파싱 (issue #9) | [Discovery](./dcy-013_20260608_kis-external-cancel-pending-sell-stuck.md) | confirmed |
| dcy-014_20260608_trading-date-configurable-timezone-kst | 20260608 | 당일 슬롯 생성 기준 시간을 KST(설정 가능)로 분리 — trading_date 타임존 분리 + 일자 경계 preallocation (issue #14) | [Discovery](./dcy-014_20260608_trading-date-configurable-timezone-kst.md) | confirmed |
| dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap | 20260608 | portfolio-hub 404(미등록 포트폴리오)가 retryable 로 분류되어 백오프·상한 없이 영구 재시도됨 (issue #13) | [Discovery](./dcy-015_20260608_ledger-outbox-permanent-error-backoff-cap.md) | confirmed |
| dcy-016_20260608_operator-cli-rpc-commands | 20260608 | 운영자 CLI 명령 확장 — universe/슬롯 확인 + enter/exit + pause/resume (데몬 RPC 통일) | [Discovery](./dcy-016_20260608_operator-cli-rpc-commands.md) | confirmed |
| dcy-017_20260608_watchdog-ownership-visibility | 20260608 | 전략 이탈로 슬롯이 내부 청산 루틴(워치독) 소유로 넘어간 상태를 가시화 — CLI/RPC vs Redis projection (issue #12) | [Discovery](./dcy-017_20260608_watchdog-ownership-visibility.md) | confirmed |
| dcy-018_20260608_ledger-response-cash-position-writeback | 20260608 | ledger write 응답의 cash/quantity 를 portfolio 스냅샷에 write-back — option D 활성화 + 수량 source 통일 (운영 환류) | [Discovery](./dcy-018_20260608_ledger-response-cash-position-writeback.md) | confirmed |
| dcy-019_20260609_slot-entry-count-and-closed-reallocation | 20260609 | slot entry_count 추가 및 closed 슬롯 재할당 정책 (issue #18) | [Discovery](./dcy-019_20260609_slot-entry-count-and-closed-reallocation.md) | confirmed |
| dcy-020_20260610_asset-centric-slot-model | 20260610 | 자산 중심 슬롯 모델 재설계 — portfolio-hub universe 와 1:1 슬롯 + 활성 슬롯 개수 제어 (일반 슬롯 풀 모델 대체) | [Discovery](./dcy-020_20260610_asset-centric-slot-model.md) | confirmed |
| dcy-021_20260610_position-sizing-target-intent | 20260610 | 포지션 사이징 — 활성 자산 슬롯 보유 수량 변경(HOLDING→HOLDING)·부분 매도·선언형 target intent (issue #1, dcy-020 OQ-4 후속) | [Discovery](./dcy-021_20260610_position-sizing-target-intent.md) | confirmed |
| dcy-022_20260610_stuck-pending-order-recovery | 20260610 | 전략 발신 intent 실행 정책 — 주문 timeout(TIF)/슬리피지 허용 + 미체결 고착 백스톱 (issue #21, dcy-020/021 모델 전제) | [Discovery](./dcy-022_20260610_stuck-pending-order-recovery.md) | confirmed |
| dcy-023_20260610_allocated-capital-dead-field-removal | 20260610 | allocated_capital 죽은 필드 제거 — `enter --capital` 옵션 + slot/intent/snapshot 계약 정리 (issue #22) | [Discovery](./dcy-023_20260610_allocated-capital-dead-field-removal.md) | confirmed |
| dcy-024_20260611_safety-sell-disable-mode | 20260611 | safety-sell 자율 청산 비활성화 모드 — per-instance TOML `safety_sell_enabled=false` 시 워커 미기동 (issue #15) | [Discovery](./dcy-024_20260611_safety-sell-disable-mode.md) | confirmed |
| dcy-025_20260611_strategy-read-surface-slots-req | 20260611 | 전략 read 표면 재설계 — `trex.snapshot.req`→`trex.slots.req` + 슬롯 상태 필터 + 슬롯 단일 행(assets[]/available_slots/watchdog[] 흡수·제거) | [Discovery](./dcy-025_20260611_strategy-read-surface-slots-req.md) | confirmed |
| dcy-026_20260612_sell-transaction-tax-and-fee-policy | 20260612 | 매도 증권거래세 누락 + 수수료 정책 정정 — `tax=0.0` 하드코딩 제거 + 단일 정책 지점 (issue #24) | [Discovery](./dcy-026_20260612_sell-transaction-tax-and-fee-policy.md) | confirmed |
| dcy-027_20260612_long-term-strategy-slot-operation | 20260612 | 중장기 매매 전략용 슬롯 운영 도입 — 구현 위치 결정 + 멀티데이 리스크 규칙 + 운영 프로파일 (issue #20, dcy-020 Out-of-Scope 이월) | [Discovery](./dcy-027_20260612_long-term-strategy-slot-operation.md) | confirmed |
| dcy-028_20260615_slot-strategy-state-store | 20260615 | Slot Strategy-State 저장소 — per-strategy opaque state store (read/write CAS/close 삭제, issue #26 C-1/C-2/C-3) | [Discovery](./dcy-028_20260615_slot-strategy-state-store.md) | confirmed |
| dcy-029_20260616_operational-observability-logging-and-pnl-view | 20260616 | 운영 관측성 — 주문 결과 로깅 + 로그 타임스탬프(KST) + 청산사유 분류 + 슬롯/PnL 조회 뷰 + 운영자 런북 (issue #29) | [Discovery](./dcy-029_20260616_operational-observability-logging-and-pnl-view.md) | confirmed |
| dcy-030_20260616_slippage-limit-price-tick-size-snapping | 20260616 | slippage 지정가 KRX 호가단위 미반영 → KIS 거부 (tick-size 스냅, 상한 준수 방향, issue #28) | [Discovery](./dcy-030_20260616_slippage-limit-price-tick-size-snapping.md) | confirmed |
| dcy-031_20260619_event-publisher-connection-reliability | 20260619 | 이벤트 발행 연결 신뢰성 — idle heartbeat 사망 후 첫 발행 유실 + DLQ 가시성 (OPS-005) | [Discovery](./dcy-031_20260619_event-publisher-connection-reliability.md) | confirmed |
| dcy-032_20260621_fill-missing-price-cost-basis-backfill | 20260621 | 체결가 누락(브로커 평균가 0/None) → 슬롯 평단·원장 cost basis 0 오염(무경고·영구) → 지정가 백필 + 로깅 + 알람 검토 | [Discovery](./dcy-032_20260621_fill-missing-price-cost-basis-backfill.md) | confirmed |
| dcy-033_20260621_suite-alarm-delivery-notifier | 20260621 | 제품군 알람 전달 체계 — 모듈 발신 + 중앙 notifier(trex-notifier) → 텔레그램(P1 아웃바운드). envelope 계약+전송(trex.alerts AMQP 확정)+등급/상관억제/하트비트 | [Discovery](./dcy-033_20260621_suite-alarm-delivery-notifier.md) | confirmed |
| dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart | 20260625 | 슬롯↔실계좌 차단형 reconcile + sync/verify 명령 — 중장기 cold-start(기존 보유 onboarding). B/C 불일치→슬롯 차단(durable warning) + pause∨warning 게이트 + 운영자 sync/verify (M3) | [Discovery](./dcy-034_20260625_slot-account-blocking-reconcile-sync-coldstart.md) | confirmed |
| dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load | 20260628 | 그날-시작 KIS 인증정보 fetch(portfolio-hub) + fail-safe 거래 로드 — 진실원천 부재 시 신규 거래 미개시, 장중 무중단 (교차 발원 portfolio-hub dcy-010) | [Discovery](./dcy-036_20260628_daystart-credential-fetch-failsafe-trading-load.md) | confirmed |
| dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix | 20260714 | HOTFIX — fetched credential 운영자 broker truth 정합 및 managed non-holding divergence 분리 (#39, #40, #41) | [Discovery](./dcy-037_20260714_operator-broker-truth-and-managed-slot-divergence-hotfix.md) | confirmed |
| dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding | 20260716 | P1 — portfolio-hub cost-basis response contract 정합 및 안전한 sync 평균가 seed | [Discovery](./dcy-038_20260716_cost-basis-response-contract-and-safe-sync-seeding.md) | confirmed |
| dcy-039_20260727_intent-venue-end-to-end-propagation | 20260727 | 전략 intent venue를 슬롯·주문·취소까지 전달 | [Discovery](./dcy-039_20260727_intent-venue-end-to-end-propagation.md) | confirmed |
| dcy-041_20260729_broker-uncertainty-and-adapter-retirement | 20260729 | Core 브로커 불확실 상태 구분과 로컬 KIS 어댑터 축소 (#44, #45) | [Discovery](./dcy-041_20260729_broker-uncertainty-and-adapter-retirement.md) | confirmed |
| dcy-042_20260729_shared-account-capital-allocation-policy | 20260729 | 메가 스윙과 탑50 스윙 공통 계좌 자본배분 정책 | [Discovery](./dcy-042_20260729_shared-account-capital-allocation-policy.md) | confirmed |