# BAT-053: Implementation ## 변경 파일 목록 ### 신규/확장 모듈 | 파일 | 변경 유형 | 관련 REQ | | --- | --- | --- | | `trex_core/application/portfolio.py` | 확장 | REQ-112 | | `trex_core/adapters/portfolio/hub_read.py` | 확장 | REQ-112 | | `trex_core/adapters/portfolio/stub_read.py` | 확장 | REQ-112 | | `trex_core/runtime/config.py` | 확장 | REQ-113 | | `trex_core/application/snapshot_scheduler.py` | 확장 | REQ-113/114/115 | | `trex_core/application/intents.py` | 확장 | REQ-114 | | `trex_core/runtime/daemon.py` | 확장 (조립) | REQ-113/114 | ### 신규 테스트 | 파일 | 관련 REQ | | --- | --- | | `tests/adapters/portfolio/test_hub_read.py` (확장) | REQ-112 AC-1~7 | | `tests/adapters/portfolio/test_stub_read.py` (확장) | REQ-112 AC-6 | | `tests/runtime/test_instance_config.py` (확장) | REQ-113 AC-1,7 | | `tests/application/test_snapshot_scheduler.py` (확장) | REQ-114 AC-1~9, REQ-115 AC-3/6 | ## REQ별 구현 요약 ### REQ-112: BrokerCredentialPayload + TradingContext + 포트 메서드 + 어댑터 - `application/portfolio.py`: - `BrokerCredentialPayload(account_ref, app_key, app_secret, credential_version)` frozen dataclass 추가 - `TradingContext(broker, trading_enabled)` frozen dataclass 추가 - `PortfolioHubReadPort.fetch_broker_credential() -> TradingContext` 포트 메서드 추가 - `adapters/portfolio/hub_read.py`: - `whoami()` 내부 로직을 `_fetch_user_info() -> dict` 헬퍼로 추출 — 외부 동작 불변(AC-5 회귀 보존) - `fetch_broker_credential()` 구현: broker null → TradingContext(broker=None), credential_payload null → TradingContext(broker=None), 성공 → BrokerCredentialPayload 반환 - NFR-2: app_secret 평문 로그 없음 — 어댑터 내 로깅 코드에 app_secret 미포함 - `adapters/portfolio/stub_read.py`: - `trading_context: Optional[TradingContext]`, `fetch_broker_raises: bool` 인자 추가 - `fetch_broker_credential()` stub 구현 — 네트워크 없이 결정론적 반환/예외 ### REQ-113: credential_fetch_enabled opt-in + break-glass - `runtime/config.py`: - `DaemonInstanceConfig.credential_fetch_enabled: bool = False` 추가 (TOML opt-in, 기본 False) - `DaemonInstanceConfig.from_toml()`: `credential_fetch_enabled` 파싱 (부재 시 False) - `DaemonConfig.credential_fetch_enabled: bool = False` 추가 - `DaemonConfig.from_config()`: `inst.credential_fetch_enabled` → `DaemonConfig` 전파 - `DaemonConfig.from_env()`: 변경 없음 → 항상 False (AC-1 회귀 0) - `application/snapshot_scheduler.py`: - 생성자 `credential_fetch_enabled: bool = False` 인자 추가 - 초기화 시 `TREX_KIS_BROKER_BREAK_GLASS` env 감지(break-glass 플래그) - break-glass: INFO 로그 후 env-only 경로(fetch 없이 entry_allowed=True) ### REQ-114: 그날-시작 로드 게이트 + fail-closed + 알람 - `application/snapshot_scheduler.py`: - `_entry_allowed: bool = True` 상태 추가 - `_maybe_fetch_credential()`: fail-closed 3조건 처리 + CRITICAL 알람 `DAY_START_LOAD_FAILED` 발신 - `run_once()` 선두: `credential_fetch_enabled=True`이면 `_maybe_fetch_credential()` 호출 - `get_entry_allowed() -> bool` 노출 - `application/intents.py`: - `IntentService` 생성자 `entry_allowed_fn: Optional[Callable[[], bool]] = None` 추가 - `_admit_buy()` 선두: `entry_allowed_fn is not None and not entry_allowed_fn()` → reject `day_start_load_failed` - SellIntent(`_admit_sell`) 변경 없음 — 청산 경로 영향 없음 (AC-5) - `runtime/daemon.py`: - `SnapshotScheduler` 생성 후 `entry_allowed_fn = scheduler.get_entry_allowed`를 IntentService에 주입 - `credential_fetch_enabled=False`(기본) → `entry_allowed_fn=None` → 게이트 비활성 ### REQ-115: 그날 고정 캐시 + 장중 무중단 - `application/snapshot_scheduler.py`: - `_cached_credential: Optional[BrokerCredentialPayload]`, `_credential_date: Optional[date]` 추가 - `_maybe_fetch_credential()`: `_credential_date == today` 시 재fetch 생략 (그날 고정 캐시, AC-3) - 성공 시 캐시 저장 + INFO 로그(account_ref/trading_enabled/credential_version; app_secret 미포함) - Redis 등 외부 저장소 기록 없음 — 메모리 한정 (AC-5) ## 설계 결정 반영 확인 - D1(entry_allowed 위치): SnapshotScheduler 내 + IntentService callable 주입 — 구현됨 - D2(credential_fetch_enabled 전파): DaemonInstanceConfig → DaemonConfig → SnapshotScheduler — 구현됨 - D3(우선순위 로직 위치): SnapshotScheduler._maybe_fetch_credential() — 구현됨 - D4(그날 고정 캐시): `_credential_date` 비교로 동일 날짜 재fetch 방지 — 구현됨 - D5(중장기 후크): BAT-053에서는 단기 07:30 경로(run_once())만 구현. 중장기는 동일 run_once() 경유로 처리됨 — 확인 - D6(AlarmEmitter): publish_alarm("DAY_START_LOAD_FAILED", ...) 직접 호출 패턴 — 구현됨 ## 주요 설계 메모 - `credential_fetch_enabled=False`(기본)일 때: `_maybe_fetch_credential()` 미호출 → `_entry_allowed=True` 고정 → IntentService `entry_allowed_fn=None` → 게이트 완전 비활성. 기존 동작 byte-for-byte 보존(회귀 0). - KISBroker 재조립(fetch 결과를 실제 KIS API에 적용)은 BAT-053 범위 외 — M3 별도 batch. 현재 `_cached_credential`이 메모리에 저장되어 있으며 다음 batch에서 KISBroker 재초기화에 활용. - REQ-115 AC-2(ledger_outbox 무손실): 기존 REQ-019 구현 속성 재확인. 별도 구현 불필요, 기존 테스트 그린(745 passed).