feat(signal_v2-phase3a): main.py lifespan integrates KIS client + WS
AppContext extended with kis_client + kis_ws. lifespan: - If KIS_APP_KEY set: create KISClient + KISWebSocket, fetch portfolio, subscribe WebSocket H0STASP0 for holdings. - If unset: WARNING log, signal_v2 still serves /health (no KIS data). - Shutdown closes kis_ws → kis_client → stock client in order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,9 @@ from fastapi import FastAPI
|
|||||||
|
|
||||||
from signal_v2 import state as state_mod
|
from signal_v2 import state as state_mod
|
||||||
from signal_v2.config import get_settings
|
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.rate_limit import SignalDedup
|
||||||
from signal_v2.stock_client import StockClient
|
from signal_v2.stock_client import StockClient
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ class AppContext:
|
|||||||
dedup: SignalDedup | None = None
|
dedup: SignalDedup | None = None
|
||||||
shutdown: asyncio.Event | None = None
|
shutdown: asyncio.Event | None = None
|
||||||
poll_task: asyncio.Task | None = None
|
poll_task: asyncio.Task | None = None
|
||||||
|
kis_client: KISClient | None = None
|
||||||
|
kis_ws: KISWebSocket | None = None
|
||||||
|
|
||||||
|
|
||||||
_ctx = AppContext()
|
_ctx = AppContext()
|
||||||
@@ -32,12 +36,43 @@ async def lifespan(app: FastAPI):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
"WEBAI_API_KEY not configured — stock API calls will fail with 401"
|
"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.client = StockClient(settings.stock_api_url, settings.webai_api_key)
|
||||||
_ctx.dedup = SignalDedup(settings.db_path)
|
_ctx.dedup = SignalDedup(settings.db_path)
|
||||||
_ctx.shutdown = asyncio.Event()
|
_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(
|
_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
|
yield
|
||||||
@@ -54,6 +89,10 @@ async def lifespan(app: FastAPI):
|
|||||||
await _ctx.poll_task
|
await _ctx.poll_task
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
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:
|
if _ctx.client is not None:
|
||||||
await _ctx.client.close()
|
await _ctx.client.close()
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,20 @@ def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog):
|
|||||||
with TestClient(main_mod.app) as client:
|
with TestClient(main_mod.app) as client:
|
||||||
client.get("/health")
|
client.get("/health")
|
||||||
assert any("WEBAI_API_KEY" in rec.message for rec in caplog.records)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user