docs(dcy-036): draft-req + confirm-req — REQ-112~115 Approved (그날-시작 KIS fetch + fail-safe 로드)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-28 10:12:18 +09:00
co-authored by Claude Opus 4.8
parent 9312ac4401
commit f9c4fb4eba
6 changed files with 374 additions and 19 deletions
@@ -0,0 +1,97 @@
# req-112: portfolio-hub broker credential read 포트 메서드 + HTTP 어댑터
- Status: Approved
- Type: Integration
- Priority: High
- Owner: trex-core-dev
## Intent
- 현재 `PortfolioHubReadAdapter.whoami()`는 `GET /v1/user_info`에서 `daemon_id`만 읽는다. dcy-036 C1에 따라 같은 엔드포인트가 daemon-scoped 단일 `broker` 객체(`account_ref`, `app_key`, `app_secret`, `credential_version`)와 `trading_enabled` 필드를 함께 반환하도록 확장된 portfolio-hub 계약(REQ-049, dcy-010)을 소비해야 한다.
- 이 REQ는 트랜스포트 레이어 — 포트 인터페이스 정의, HTTP 어댑터 구현, 테스트용 stub — 만 다루며, KISConfig 구성 로직(REQ-113)과 로드 게이트(REQ-114)의 기반을 제공한다.
## Requirement
- `trex_core/application/portfolio.py`에 `BrokerCredentialPayload` 데이터클래스를 추가한다: `account_ref: str`, `app_key: str`, `app_secret: str`, `credential_version: int | None = None`.
- `PortfolioHubReadPort` 프로토콜에 `fetch_broker_credential() -> BrokerCredentialPayload | None` 메서드를 추가한다.
- 반환 값 None: portfolio-hub 응답 `broker: null` (해당 daemon에 인증정보 없음).
- `trading_enabled` 필드도 함께 읽어 반환해야 하므로, 포트 메서드는 `TradingContext` 데이터클래스를 반환한다: `broker: BrokerCredentialPayload | None`, `trading_enabled: bool`.
- `PortfolioHubReadAdapter`의 기존 `whoami()` 메서드를 내부적으로 `GET /v1/user_info`를 공유하는 내부 헬퍼(`_fetch_user_info()`)로 리팩토링하고, `fetch_broker_credential()` 구현이 이를 재사용하도록 한다. 외부 동작은 변경하지 않는다(`whoami()` 시그니처·예외 동작 불변, 회귀 차단 NFR-1).
- `fetch_broker_credential()`은 다음 규칙을 따른다:
- `broker: null` 응답 → `TradingContext(broker=None, trading_enabled=trading_enabled)` 반환.
- `credential_payload: null`(복호화 실패) → `TradingContext(broker=None, trading_enabled=trading_enabled)` 반환(정보 부재로 처리).
- 네트워크 오류 / 비-2xx / JSON 파싱 실패 → `PortfolioHubUnavailableError` raise.
- HTTP 403(`daemon_id` 없는 토큰) → `PortfolioHubUnavailableError` raise.
- `trex_core/adapters/portfolio/stub_read.py`의 stub에도 `fetch_broker_credential()` 결정론 stub을 추가한다.
- `app_secret` 필드는 어댑터 내에서 로그에 직접 출력하지 않는다(NFR-2 보안 — 마스킹).
## Acceptance Criteria
1. Given daemon_id에 매칭된 broker 인증정보가 있고 `trading_enabled: true`인 `/v1/user_info` 응답, When `fetch_broker_credential()` 호출, Then `TradingContext(broker=BrokerCredentialPayload(account_ref=..., app_key=..., app_secret=...), trading_enabled=True)` 반환.
2. Given `broker: null` 응답(인증정보 없음), When `fetch_broker_credential()` 호출, Then `TradingContext(broker=None, trading_enabled=<응답값>)` 반환 (예외 없음).
3. Given `credential_payload: null`(복호화 실패) 응답, When `fetch_broker_credential()` 호출, Then `TradingContext(broker=None, trading_enabled=<응답값>)` 반환 (예외 없음, 인증정보 부재로 처리).
4. Given 네트워크 오류 또는 비-2xx(403/500 등) 응답, When `fetch_broker_credential()` 호출, Then `PortfolioHubUnavailableError` raise.
5. Given `fetch_broker_credential()` 구현 이후, When `whoami()` 호출, Then 기존 `whoami()` 계약(daemon_id str 반환, PortfolioHubUnavailableError on failure)이 변경되지 않는다 (회귀 없음).
6. Given stub_read.py stub, When `fetch_broker_credential()` 호출, Then 미리 설정한 고정 `TradingContext`를 네트워크 없이 반환한다.
7. Given `fetch_broker_credential()` 구현, When 에러/성공 로그 출력 확인, Then `app_secret` 값이 평문으로 로그에 출력되지 않는다.
8. Given import sweep, When `trex_core/domain`을 검사, Then 신규 포트/어댑터 관련 IO/네트워크 의존이 domain에 없다 (도메인 순수성 불변).
## Impacted Area
- Docs: 없음 (baseline 문서 갱신은 dcy-036 scope 밖)
- Modules:
- `trex_core/application/portfolio.py` — `BrokerCredentialPayload`, `TradingContext` 데이터클래스 신규 정의; `PortfolioHubReadPort`에 `fetch_broker_credential()` 메서드 추가
- `trex_core/adapters/portfolio/hub_read.py` — `_fetch_user_info()` 내부 헬퍼 추출, `fetch_broker_credential()` 구현 추가; `whoami()` 동작 보존
- `trex_core/adapters/portfolio/stub_read.py` — `fetch_broker_credential()` stub 추가
- Tests:
- `tests/adapters/portfolio/test_hub_read.py` — AC-1~5, AC-7 케이스 추가
- `tests/adapters/portfolio/test_stub_read.py` — AC-6 stub 케이스 추가
- `tests/domain/test_domain_import_sweep.py` — AC-8 회귀(기존 sweep 재실행)
## Notes
- 소비 계약: portfolio-hub `req-049_user-info-daemon-scoped-broker-delivery.md` (Implemented, dcy-010 FR-2). 응답 스키마: `broker: { account_ref, credential_payload: { app_key, app_secret } | null, credential_version, ... } | null` + `trading_enabled: bool`.
- `TradingContext`를 별도 데이터클래스로 도입하는 이유: 포트 메서드가 broker 인증정보와 `trading_enabled`를 한 번의 호출로 함께 반환해야 불필요한 중복 HTTP 호출을 피할 수 있기 때문(원자적 읽기).
- OQ-4(그날-시작 로드 실패 알람 trex-notifier 연동): Deferred — 알람 발신은 REQ-114 Notes에서 다룬다.
- 선행 의존: portfolio-hub dcy-010 / REQ-049 배포 완료 여부에 따라 실 통합 테스트 가능 시점이 달라진다. CI 단계에서는 stub 기반 단위 테스트로 AC-1~8 검증.
- Source: dcy-036 C1, FR-1, dcy-010 OQ-2 Answered.
## Change Log
### CHG-20260628-01
- Date: 2026-06-28
- Author: trex-core-dev
#### Change Summary
- 신규 생성 (Proposed) — dcy-036 C1 산출물.
#### Intent
- dcy-036 FR-1(그날-시작 portfolio-hub `/v1/user_info`에서 KIS 인증정보 fetch)의 트랜스포트 계층을 REQ로 정규화한다. KISConfig 구성(REQ-113) 및 로드 게이트(REQ-114)의 선행 의존.
#### Acceptance Criteria Delta
- Added:
- AC-1~8 전체 (신규 초안)
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: 없음
- Modules:
- `portfolio.py` 포트 확장 (BrokerCredentialPayload, TradingContext, fetch_broker_credential)
- `hub_read.py` 어댑터 확장 (_fetch_user_info 헬퍼, fetch_broker_credential 구현)
- `stub_read.py` stub 확장
- Tests:
- `test_hub_read.py`, `test_stub_read.py`, `test_domain_import_sweep.py`
#### Delivery Trace Delta
- Batches: 미정 (suggest-batch-reqs 대기)
- Releases: 미정
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req 승인 게이트 후 REQ-112~115 일괄 batch 배정 검토.
#### Validation Plan
- pytest로 AC-1~8 각 케이스 확인. app_secret 마스킹(AC-7)은 로그 캡처 검증. domain sweep(AC-8)은 기존 test_domain_import_sweep.py 재실행.
### CHG-20260628-02
- Date: 2026-06-28
- Author: 팀장(Claude)
- confirm-req: `Proposed` → `Approved`. dcy-036 FR-1/C1 추적. TradingContext 원자 read(broker+trading_enabled 1회 호출) 설계 타당 — /v1/user_info 단일 응답에 정합. whoami 회귀보존(AC-5)·도메인 순수성(AC-8)·app_secret 마스킹(AC-7) 적절. blocker 없음. 선행 의존 portfolio-hub REQ-049(머지완료).
@@ -0,0 +1,94 @@
# req-114: 그날-시작 로드 게이트 — fail-closed 신규 진입 차단 + 알람
- Status: Approved
- Type: Integration
- Priority: High
- Owner: trex-core-dev
## Intent
- dcy-036 C2/FR-2에 따라, 그날-시작 체크포인트(단기: 07:30 KST 스냅샷, 중장기: dcy-027 운영 프로파일 주기)에서 portfolio-hub가 불가하거나 broker 인증정보 부재(`broker: null`) 또는 `trading_enabled: false`이면 그날 **신규 진입을 개시하지 않는다(fail-closed)**. 사유를 로깅하고 운영자 알람을 발신한다.
- "그날 고정" 원칙: 로드 성공 후 그날 인증정보는 캐시로 유지되며, 장중 portfolio-hub 복구 시에도 그날 재fetch하지 않는다(결정론 — REQ-115 담당).
## Requirement
- **그날-시작 체크포인트 후크**: `SnapshotScheduler.run_once()`(단기 07:30 KST 스냅샷 트리거)의 `credential_fetch_enabled = true` 경로에서, 스냅샷 전 `fetch_broker_credential()` (REQ-112)를 호출해 `TradingContext`를 획득한다. 중장기 인스턴스는 dcy-027 운영 프로파일 리밸런싱 주기의 동등한 후크 지점에서 동일 로직을 수행한다.
- **fail-closed 조건**: 다음 중 하나라도 해당하면 그날 `entry_allowed = False` 상태를 설정한다:
1. `PortfolioHubUnavailableError` (네트워크 오류 / 비-2xx / 403)
2. `TradingContext.broker is None` (인증정보 부재)
3. `TradingContext.trading_enabled is False`
- **진입 차단**: `entry_allowed = False` 상태에서 BuyIntent(신규 진입 의도)는 admission 단계에서 거부된다. 이미 진행 중인 주문(DISPATCHED 슬롯)과 청산(SellIntent) 경로는 차단하지 않는다.
- **사유 로깅**: fail-closed 진입 시 ERROR 레벨로 사유(portfolio-hub 불가 / broker null / trading_enabled false)와 인스턴스 정보를 로깅한다. app_secret은 로그에 포함하지 않는다.
- **알람 발신**: fail-closed 진입 시 CRITICAL 알람을 `AlarmEmitter`를 통해 발신한다(`trex.alerts` exchange, REQ-099/100). 알람 코드: `DAY_START_LOAD_FAILED`. 알람 페이로드에는 사유 코드와 인스턴스 키를 포함한다.
- **성공 로깅**: 로드 성공 시 INFO 레벨로 fetch 소스, account_ref(app_secret 제외), trading_enabled를 기록한다.
## Acceptance Criteria
1. Given `credential_fetch_enabled = true`이고 그날-시작 체크포인트에서 `PortfolioHubUnavailableError` 발생, When 체크포인트 실행, Then `entry_allowed = False` 설정 + ERROR 로그 + CRITICAL 알람(`DAY_START_LOAD_FAILED`) 발신.
2. Given `credential_fetch_enabled = true`이고 그날-시작 체크포인트에서 `TradingContext.broker is None`, When 체크포인트 실행, Then `entry_allowed = False` 설정 + ERROR 로그 + CRITICAL 알람 발신.
3. Given `credential_fetch_enabled = true`이고 그날-시작 체크포인트에서 `TradingContext.trading_enabled = False`, When 체크포인트 실행, Then `entry_allowed = False` 설정 + ERROR 로그 + CRITICAL 알람 발신.
4. Given `entry_allowed = False` 상태, When BuyIntent 수신, Then BuyIntent가 admission 단계에서 거부된다 (신규 진입 차단).
5. Given `entry_allowed = False` 상태, When SellIntent 수신, Then SellIntent는 차단되지 않고 정상 처리된다 (청산 경로 영향 없음).
6. Given `credential_fetch_enabled = true`이고 그날-시작 체크포인트 성공(`broker` 있음 + `trading_enabled = True`), When 체크포인트 실행, Then `entry_allowed = True` 설정 + INFO 로그(account_ref 포함, app_secret 제외).
7. Given `credential_fetch_enabled = false`, When 그날-시작 체크포인트 실행, Then `fetch_broker_credential()` 호출 없이 기존 스냅샷 로직만 수행된다 (회귀 없음, opt-in 보존).
8. Given 알람 발신 성공, When `trex.alerts` exchange 메시지 확인, Then `DAY_START_LOAD_FAILED` 코드와 instance_key를 포함한 알람 envelope가 존재한다.
9. Given fail-closed 후 `entry_allowed = False` 상태에서 portfolio-hub가 복구됨, When 그날 중 재fetch 없이 운행, Then `entry_allowed` 상태는 그날 고정(재fetch로 자동 해제 안 됨) — 다음 그날-시작 체크포인트까지 유지.
## Impacted Area
- Docs: 없음 (baseline 문서 갱신은 별도 scope)
- Modules:
- `trex_core/application/snapshot_scheduler.py` — `run_once()` 또는 그날-시작 후크 지점에 `fetch_broker_credential()` 호출 + `entry_allowed` 상태 설정 + 알람 발신 추가
- `trex_core/application/execution.py` (또는 IntentService) — BuyIntent admission에 `entry_allowed` 게이트 추가
- `trex_core/application/alarm_emitter.py` — `DAY_START_LOAD_FAILED` 알람 코드 추가(REQ-100 패턴 재사용)
- Tests:
- `tests/application/test_snapshot_scheduler.py` — AC-1~3, AC-6~9 케이스 추가 (stub fetch_broker_credential + AlarmEmitter mock)
- `tests/application/test_execution.py` (또는 IntentService 테스트) — AC-4~5 케이스 추가
## Notes
- OQ-4(그날-시작 로드 실패 알람 trex-notifier 연동): **Deferred** — 이 REQ는 `trex.alerts` exchange 발신(REQ-099/100 계약)까지만 정의한다. trex-notifier(dcy-033) 연동은 별도 REQ/Discovery로 추적한다. 알람 등급은 CRITICAL(거래 개시 불가).
- `entry_allowed` 상태 저장소: in-process 상태 변수(process-local)로 충분하다. 재시작 시 그날-시작 체크포인트가 재실행되므로 내구성 저장소 불필요.
- 중장기 인스턴스의 "그날-시작 체크포인트" 시점은 dcy-027 운영 프로파일 리밸런싱 주기의 구체적 후크 위치에 의존한다. 구현 단계에서 확인 후 조정.
- AC-9(그날 고정 상태 불변성): 장중 재fetch 없음이 REQ-115의 보완 요건이기도 하다. 이 REQ는 "진입 차단 상태의 그날 고정"만 명시하고, "장중 장애 후 인증정보 캐시 지속 운행"은 REQ-115가 담당.
- Source: dcy-036 C2, FR-2, OQ-1 Answered (단기 07:30 / 중장기 프로파일 주기), OQ-2 Answered (그날 고정).
## Change Log
### CHG-20260628-01
- Date: 2026-06-28
- Author: trex-core-dev
#### Change Summary
- 신규 생성 (Proposed) — dcy-036 C2/FR-2 산출물.
#### Intent
- dcy-036 FR-2(그날-시작 로드 시점 fail-closed 진입 차단 + 알람)를 Integration REQ로 정규화한다.
#### Acceptance Criteria Delta
- Added:
- AC-1~9 전체 (신규 초안)
- Updated: 없음
- Removed: 없음
#### Impacted Area Delta
- Docs: 없음
- Modules:
- `snapshot_scheduler.py` 그날-시작 후크 + entry_allowed 상태 + 알람
- `execution.py` / IntentService BuyIntent admission 게이트
- `alarm_emitter.py` 코드 추가
- Tests:
- `test_snapshot_scheduler.py`, `test_execution.py`
#### Delivery Trace Delta
- Batches: 미정 (suggest-batch-reqs 대기)
- Releases: 미정
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: confirm-req 후 REQ-112~115 일괄 batch 배정 검토.
#### Validation Plan
- pytest로 AC-1~9 각 케이스 확인. stub `fetch_broker_credential`과 AlarmEmitter mock으로 I/O 격리. AC-4/5(차단 범위 경계: 매수 차단 / 매도 통과)가 가장 중요한 케이스.
### CHG-20260628-02
- Date: 2026-06-28
- Author: 팀장(Claude)
- confirm-req: `Proposed` → `Approved`. dcy-036 FR-2/OQ-1/OQ-2 추적. **진입 fail-closed(BuyIntent 차단) + 청산·DISPATCHED 통과(AC-4/5)** 비대칭이 사장님 fail-safe 결정 정합 — 자본 갇힘 방지. CRITICAL 알람 기존 trex.alerts(REQ-099/100) 재사용·그날 고정(AC-9) 적절. OQ-4(trex-notifier 연동) Deferred 정당. blocker 없음.