feat(bat-054): 그날-시작 KIS 인증정보 양의 경로 주입 — KISBroker reinitialize (REQ-113 AC-2/AC-4, REQ-115 AC-1)

BAT-053 스캐폴드에서 연기된 양의 경로 완성:
- KISBroker.reinitialize(credential): app_key/app_secret/account_ref 교체 + L1 token 무효화
- SnapshotScheduler에 kis_reinit_callback 주입 지점 추가; 그날-시작 성공 시 callback 호출
- daemon.py에 _build_kis_reinit_callback() 헬퍼 + SnapshotScheduler 조립 수정
- 신규 테스트 12개 추가: 757 passed / 47 skipped (기존 745 대비 +12, 회귀 0)

안전 불변식 유지:
- credential_fetch_enabled=False(기본) 경로 byte-for-byte 보존
- fail-closed 불변식 미파괴 (reinit 실패 시 entry_allowed 유지)
- AMQP wire 계약 변경 없음

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-28 19:51:48 +09:00
co-authored by Claude Sonnet 4.6
parent a50b5e34a2
commit 6b066e19cb
10 changed files with 733 additions and 1 deletions
+32
View File
@@ -69,6 +69,38 @@ class KISBroker:
def name(self) -> str:
return "kis-mock" if self._mock else "kis-live"
# --- credential hot-swap (dcy-036 / REQ-113 AC-2 / REQ-115 AC-1) ----------
def reinitialize(self, credential) -> None:
"""Replace app_key/app_secret/account_ref with fetched day-start credential.
Called once at the day-start checkpoint (07:30 KST) after a successful
portfolio-hub fetch (REQ-113 AC-2 / REQ-115 AC-1). base_url / timeout /
allow_live / _mock are env-sourced and remain unchanged.
Thread-safety: CPython GIL makes attribute assignment atomic. This is
safe because reinitialize() is called at day-start (before trading) and
the "day-scoped fixed cache" principle (REQ-115) ensures no intraday
re-fetch. If intraday reinitialize is ever needed a lock should be
added at that time.
Access-token invalidation:
- L1 in-memory token is cleared so the next _ensure_token() call
issues a fresh token with the new credentials.
- L2 Redis cache: the new (account_ref, app_key) key will miss on
first lookup, triggering a fresh token issue. The old key expires
via TTL — no explicit delete needed for 1x-daily rotation.
Args:
credential: BrokerCredentialPayload (from trex_core.application.portfolio)
providing account_ref, app_key, app_secret.
"""
self._account_ref = credential.account_ref
self._app_key = credential.app_key
self._app_secret = credential.app_secret
# Invalidate L1 in-memory token so next call re-issues.
self._token = None
self._token_exp = datetime.min.replace(tzinfo=timezone.utc)
# --- auth ---
def _ensure_token(self) -> str:
# L1: in-memory