Merge pull request #35 from fourblocks2020/feat/dcy-036-kis-credential-inject

feat(dcy-036/bat-054): KISBroker 양의 경로 — 데몬-스코프 KIS 인증정보 실거래 주입
This commit is contained in:
2026-06-30 13:54:49 +09:00
committed by GitHub
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
@@ -50,6 +50,7 @@ class SnapshotScheduler:
alarm_emitter: Optional[AlarmEmitter] = None,
warn_store: Optional[ReconcileWarningStore] = None,
credential_fetch_enabled: bool = False,
kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None,
) -> None:
self._read = read_port
self._store = store
@@ -72,6 +73,10 @@ class SnapshotScheduler:
# dcy-036 / REQ-113/114/115: credential fetch opt-in + break-glass detection.
# credential_fetch_enabled=False (default) → env-only, no portfolio-hub fetch.
self._credential_fetch_enabled: bool = credential_fetch_enabled
# dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: optional callback to reinitialize
# KISBroker with the fetched day-start credential. None = no reinit (paper
# broker or credential_fetch_enabled=False). Set by daemon.py assembly.
self._kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = kis_reinit_callback
# REQ-113: detect break-glass at init time (env var set by operator).
self._break_glass: bool = (
os.environ.get("TREX_KIS_BROKER_BREAK_GLASS", "").lower()
@@ -206,6 +211,26 @@ class SnapshotScheduler:
ctx.broker.credential_version,
self._instance_key,
)
# dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: reinitialize KISBroker with
# fetched credential so intraday KIS API calls use the day-start creds.
# Failure does NOT flip entry_allowed — the broker runs on existing env
# credentials until next checkpoint (graceful degrade, not fail-closed).
if self._kis_reinit_callback is not None:
try:
self._kis_reinit_callback(ctx.broker)
_log.info(
"DAY_START KISBroker reinitialized with fetched credential "
"(account_ref=%s, instance=%s)",
ctx.broker.account_ref,
self._instance_key,
)
except Exception as exc: # noqa: BLE001
_log.error(
"DAY_START KISBroker reinitialize failed: %s (instance=%s) "
"— trading continues with existing env credential",
exc.__class__.__name__,
self._instance_key,
)
def _emit_day_start_alarm(self, *, reason: str) -> None:
"""Publish CRITICAL DAY_START_LOAD_FAILED alarm (REQ-114)."""
+24
View File
@@ -122,6 +122,27 @@ def _build_transport(config: DaemonConfig) -> MessagingTransport:
return RabbitMQTransport(mq.url, timeout=mq.timeout)
def _build_kis_reinit_callback(config: DaemonConfig, broker: BrokerPort):
"""Build a KISBroker reinitialize callback for the day-start credential hook.
Returns broker.reinitialize if all conditions are met:
- config.broker == 'kis' (not paper)
- config.credential_fetch_enabled == True (opt-in)
- broker has a reinitialize() method (KISBroker)
Returns None otherwise so SnapshotScheduler skips reinit (backward compat).
dcy-036 / REQ-113 AC-2 / REQ-115 AC-1.
"""
if config.broker != "kis":
return None
if not config.credential_fetch_enabled:
return None
reinit = getattr(broker, "reinitialize", None)
if reinit is None or not callable(reinit):
return None
return reinit
def _build_portfolio_read(config: DaemonConfig) -> PortfolioHubReadPort:
"""portfolio-hub HTTP read adapter when configured; otherwise stub (REQ-027)."""
pf = config.portfolio_hub
@@ -216,6 +237,9 @@ class Daemon:
alarm_emitter=self.alarm_emitter, # dcy-033 / REQ-100
warn_store=self.warn_store, # BAT-050 / REQ-103: durable reconcile-warning
credential_fetch_enabled=config.credential_fetch_enabled, # dcy-036 / REQ-113
# dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: KISBroker reinit callback.
# None when broker!=kis or credential_fetch_enabled=False (backward compat).
kis_reinit_callback=_build_kis_reinit_callback(config, self.broker),
)
# dcy-036 / REQ-114: wire entry_allowed_fn after snapshot_scheduler is created.
# When credential_fetch_enabled=False (default), entry_allowed_fn=None = no gate.