REQ-112: BrokerCredentialPayload/TradingContext 데이터클래스 + PortfolioHubReadPort.fetch_broker_credential() 포트 메서드 + hub_read 어댑터(_fetch_user_info 헬퍼 추출) + stub_read 결정론 stub REQ-113: DaemonInstanceConfig/DaemonConfig.credential_fetch_enabled opt-in(기본 False) + TOML 파싱 + break-glass(TREX_KIS_BROKER_BREAK_GLASS) 감지 REQ-114: SnapshotScheduler._maybe_fetch_credential() — fail-closed 3조건(unavailable/broker-null/trading_disabled) + CRITICAL 알람 DAY_START_LOAD_FAILED + IntentService entry_allowed_fn 게이트(BuyIntent 차단, SellIntent 통과) REQ-115: 그날 고정 캐시(_cached_credential/_credential_date) — 동일 날짜 재fetch 방지 + app_secret 메모리 한정(Redis 미기록) 스텁 기반 구현: 실 portfolio-hub/네트워크 없이 AC 전부 단위 테스트. 회귀: credential_fetch_enabled=False(기본) 경로 byte-for-byte 보존. 745 passed, 47 skipped (0 failed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
7.9 KiB
BAT-053: Design
Key Design Decisions
D1: entry_allowed 상태 위치 — SnapshotScheduler + 공유 callable
결정: SnapshotScheduler에 entry_allowed: bool = True (opt-in=False 기본 통과) 상태를 두고, IntentService에 entry_allowed_fn: Callable[[], bool] | None 게이트를 주입한다.
근거:
entry_allowed는 그날-시작 체크포인트(SnapshotScheduler.run_once())에서 설정되므로 소유자는 SnapshotScheduler가 자연스럽다.- IntentService는 이미
warn_store,max_active_slots등 주입형 게이트 패턴을 사용한다. 동일 패턴으로entry_allowed_fn: Optional[Callable[[], bool]] = None을 추가하면 기존 코드 변경 최소화. entry_allowed_fn=None(기본)이면 게이트 완전 통과 →credential_fetch_enabled=False(기본)일 때 기존 동작 byte-for-byte 보존(회귀 0).- daemon.py에서
scheduler.get_entry_allowed메서드를IntentService에 주입하는 방식으로 조립.
대안 검토:
- 별도
DayStartState객체: 추가 클래스 생성 필요, SnapshotScheduler와 결합 불필요하게 증가. - IntentService 내부 setter: 의존 방향 역전(application이 application 내부를 조율하는 순환 구조) — 거부.
D2: credential_fetch_enabled 플래그 전파 경로
결정: DaemonInstanceConfig.credential_fetch_enabled: bool = False → DaemonConfig.from_config()에서 inst.credential_fetch_enabled를 읽어 DaemonConfig.credential_fetch_enabled: bool = False에 설정. SnapshotScheduler 생성자에 credential_fetch_enabled: bool = False 인자 추가.
근거:
- TOML →
DaemonInstanceConfig→DaemonConfig→SnapshotScheduler흐름이 기존safety_sell_enabled,watchdog_timeout_sec패턴과 동일. 일관성 유지. from_env()는 변경 없음(alwaysFalse).from_config()만 TOML 값을 읽어 전파.
D3: REQ-113 fetch 소스 우선순위 구현 위치
결정: 우선순위 로직은 DaemonConfig.from_config()에 두지 않고 SnapshotScheduler.run_once()의 그날-시작 후크에 둔다.
근거:
- REQ-113 Notes: "
base_url/timeout/allow_live는 env 유지". fetch는 app_key/app_secret/account_ref만 가져온다. DaemonConfig.from_config()는 정적 구성 해결 단계 — 네트워크 I/O(fetch)를 여기에 두면 의존성 역전 위반(runtime이 portfolio-hub를 직접 호출).SnapshotScheduler.run_once()는 이미 portfolio-hub를 호출하는 지점이고,credential_fetch_enabled=True일 때 추가 fetch가 자연스럽다.- break-glass(
TREX_KIS_BROKER_BREAK_GLASS=true) 감지는SnapshotScheduler초기화 시os.environ을 읽어_break_glass: bool플래그로 보관. - fetch 결과(
BrokerCredentialPayload)는SnapshotScheduler._cached_credential에 그날 고정 캐시. - KISConfig 재조립 경로:
SnapshotScheduler에kis_config_callback: Optional[Callable[[BrokerCredentialPayload], None]]를 주입. daemon.py에서 이 callback이KISBroker를 재초기화하거나 config를 갱신하는 역할을 한다. 단, M3에서 중장기 인스턴스 추가 전까지는 단기 07:30 로드에서만 필요하고, BAT-053 범위에서는entry_allowed플래그 설정 + 알람까지만 구현. KISBroker 재조립은 M3 별도 batch.
BAT-053 범위 명확화:
- REQ-113 AC-1~7 중 AC-2(fetch 결과로 KISConfig 구성)의 "KISBroker에 실제 적용" 부분은 M3 KISBroker 재조립 batch에 defer. 이번 batch에서는 fetch→캐시→entry_allowed 게이트 + 알람까지.
- 단, AC-2 테스트는 TradingContext가 올바르게 반환됨을 stub으로 검증.
D4: 그날 고정 캐시 설계 (REQ-115)
결정: SnapshotScheduler에 인스턴스 변수 추가:
_cached_credential: Optional[BrokerCredentialPayload] = None_credential_date: Optional[date] = None_entry_allowed: bool = True(opt-in=False면 항상 True = 차단 안 함)
run_once() 시 credential_fetch_enabled=True이면 _maybe_fetch_credential() 메서드 호출.
_maybe_fetch_credential():
_credential_date == today→ 그날 고정 캐시 반환(재fetch 안 함, REQ-115 AC-3)- break-glass → env-only, 캐시 없음,
_entry_allowed=True fetch_broker_credential()호출 → 성공: 캐시 +_entry_allowed=True; 실패(3가지 조건):_entry_allowed=False+ ERROR + CRITICAL 알람
get_entry_allowed() -> bool 메서드 노출 → daemon.py에서 IntentService.entry_allowed_fn에 주입.
D5: 중장기 인스턴스 후크 위치 (REQ-114 Notes)
결정: BAT-053에서는 단기 07:30 KST 경로(SnapshotScheduler.run_once())만 구현. 중장기 인스턴스(dcy-027 리밸런싱 주기)의 동등 후크는 BAT-053 구현 후 그 위치를 확인하고 설계에 명시. 현재 코드상 중장기 후크는 SnapshotScheduler._maybe_tick()과 run_once()를 공유하므로 credential_fetch_enabled=True이면 동일 경로로 처리됨. dcy-027 추가 인스턴스가 별도 run_once() 트리거 주기를 가질 경우 그때 조정.
D6: AlarmEmitter DAY_START_LOAD_FAILED
결정: AlarmEmitter 자체는 변경 없음(코드는 문자열 파라미터로 전달). SnapshotScheduler에서 self._alarm_emitter.publish_alarm("DAY_START_LOAD_FAILED", ...) 직접 호출. ALARM-INVENTORY.md 또는 별도 코드 상수 추가는 BAT-053 범위 외(dcy-033/REQ-100 패턴 재사용).
Module Interface Summary
application/portfolio.py 추가
@dataclass(frozen=True)
class BrokerCredentialPayload:
account_ref: str
app_key: str
app_secret: str
credential_version: Optional[int] = None
@dataclass(frozen=True)
class TradingContext:
broker: Optional[BrokerCredentialPayload]
trading_enabled: bool
# PortfolioHubReadPort에 추가:
def fetch_broker_credential(self) -> TradingContext: ...
adapters/portfolio/hub_read.py 변경
whoami()내부 로직을_fetch_user_info() -> dict헬퍼로 추출whoami()=_fetch_user_info()에서daemon_id필드 반환 (외부 동작 불변)fetch_broker_credential()=_fetch_user_info()에서broker,trading_enabled파싱
adapters/portfolio/stub_read.py 변경
trading_context: Optional[TradingContext] = None생성자 인자 추가fetch_broker_credential()stub 구현
runtime/config.py 변경
DaemonInstanceConfig.credential_fetch_enabled: bool = False추가 +from_toml()파싱DaemonConfig.credential_fetch_enabled: bool = False추가DaemonConfig.from_config():inst.credential_fetch_enabled→DaemonConfig.credential_fetch_enabled전파
application/snapshot_scheduler.py 변경
- 생성자:
credential_fetch_enabled: bool = False인자 추가 - 인스턴스 변수:
_cached_credential,_credential_date,_entry_allowed _maybe_fetch_credential()비공개 메서드 추가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()→ rejectday_start_load_failed
runtime/daemon.py 변경
SnapshotScheduler생성 시credential_fetch_enabled=config.credential_fetch_enabled전달IntentService생성 시entry_allowed_fn=self.snapshot_scheduler.get_entry_allowed전달 → SnapshotScheduler가 먼저 생성되어야 함 (순서 조정 필요 확인)
Layer Compliance
domain/변경 없음 — 도메인 순수성 불변application/portfolio.py: 새 dataclass(frozen=True) — IO/네트워크 없음 (import sweep 통과)application/snapshot_scheduler.py: portfolio 포트 호출(이미 포트 인터페이스) — 신규 import 없음application/intents.py: callable 주입 — 추가 import 없음adapters/: 구체 구현만 변경runtime/: 조립 레이어 변경