diff --git a/signal_v2/main.py b/signal_v2/main.py index 6f7a75e..a42e2b6 100644 --- a/signal_v2/main.py +++ b/signal_v2/main.py @@ -8,7 +8,9 @@ from fastapi import FastAPI from signal_v2 import state as state_mod from signal_v2.config import get_settings -from signal_v2.pull_worker import poll_loop +from signal_v2.kis_client import KISClient +from signal_v2.kis_websocket import KISWebSocket +from signal_v2.pull_worker import poll_loop, make_asking_price_callback from signal_v2.rate_limit import SignalDedup from signal_v2.stock_client import StockClient @@ -20,6 +22,8 @@ class AppContext: dedup: SignalDedup | None = None shutdown: asyncio.Event | None = None poll_task: asyncio.Task | None = None + kis_client: KISClient | None = None + kis_ws: KISWebSocket | None = None _ctx = AppContext() @@ -32,12 +36,43 @@ async def lifespan(app: FastAPI): logger.warning( "WEBAI_API_KEY not configured — stock API calls will fail with 401" ) + if not settings.kis_app_key: + logger.warning( + "KIS_APP_KEY not configured — KIS REST/WebSocket disabled" + ) _ctx.client = StockClient(settings.stock_api_url, settings.webai_api_key) _ctx.dedup = SignalDedup(settings.db_path) _ctx.shutdown = asyncio.Event() + + # KIS only if app_key configured + if settings.kis_app_key: + _ctx.kis_client = KISClient( + app_key=settings.kis_app_key, + app_secret=settings.kis_app_secret, + account=settings.kis_account, + is_virtual=settings.kis_is_virtual, + v1_token_path=settings.v1_token_path, + ) + _ctx.kis_ws = KISWebSocket( + app_key=settings.kis_app_key, + app_secret=settings.kis_app_secret, + is_virtual=settings.kis_is_virtual, + ) + # Subscribe portfolio holdings (if any) + try: + portfolio = await _ctx.client.get_portfolio() + tickers = [h["ticker"] for h in portfolio.get("holdings", []) if "ticker" in h] + cb = make_asking_price_callback(state_mod.state) + await _ctx.kis_ws.start(tickers, cb) + except Exception: + logger.exception("KIS WebSocket startup failed — continuing without realtime asking_price") + _ctx.poll_task = asyncio.create_task( - poll_loop(_ctx.client, state_mod.state, _ctx.shutdown) + poll_loop( + _ctx.client, state_mod.state, _ctx.shutdown, + kis_client=_ctx.kis_client, + ) ) yield @@ -54,6 +89,10 @@ async def lifespan(app: FastAPI): await _ctx.poll_task except asyncio.CancelledError: pass + if _ctx.kis_ws is not None: + await _ctx.kis_ws.close() + if _ctx.kis_client is not None: + await _ctx.kis_client.close() if _ctx.client is not None: await _ctx.client.close() diff --git a/signal_v2/tests/test_main.py b/signal_v2/tests/test_main.py index 058211c..78222c7 100644 --- a/signal_v2/tests/test_main.py +++ b/signal_v2/tests/test_main.py @@ -34,3 +34,20 @@ def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog): with TestClient(main_mod.app) as client: client.get("/health") assert any("WEBAI_API_KEY" in rec.message for rec in caplog.records) + + +def test_startup_warns_if_kis_app_key_missing(monkeypatch, caplog): + """KIS_APP_KEY 미설정 시 startup WARNING (KIS 호출 disabled).""" + monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local") + monkeypatch.setenv("WEBAI_API_KEY", "test-secret") + monkeypatch.delenv("KIS_APP_KEY", raising=False) + + import importlib + from signal_v2 import config as cfg + importlib.reload(cfg) + from signal_v2 import main as main_mod + importlib.reload(main_mod) + with caplog.at_level(logging.WARNING, logger="signal_v2.main"): + with TestClient(main_mod.app) as client: + client.get("/health") + assert any("KIS_APP_KEY" in rec.message for rec in caplog.records)