Files
ai-trade/ai_trade/main.py
gahusb 139e4e3382 refactor(web-ai): rename signal_v2→ai_trade, deprecate signal_v1
박재오 결정 2026-05-19 — V2를 정식 명칭 ai_trade로 graduation,
V1은 deprecated 마킹 (legacy 디렉토리 이동은 file lock 풀린 후 후속).

변경 사항:
- signal_v2/ → ai_trade/ (git mv, import 일괄 sed: signal_v2.x → ai_trade.x)
- root start.bat → legacy/start_v1.bat (V1 자동 시작 차단)
- ai_trade/start.bat 내부 uvicorn target signal_v2.main → ai_trade.main
- signal_v1/DEPRECATED.md 추가 (사용 금지 명시)
- CLAUDE.md 디렉토리 표·서버 시작 방식 갱신
- services/ 디렉토리 미래 예정 (Plan-B-Insta 작업 시 신설)

ai_trade tests 59/59 PASS 확인.

signal_v1/ 디렉토리 자체 이동(legacy/signal_v1/)은 telegram_bot.log +
data/news_snapshots.db file lock으로 보류. lock 해제 후 후속 커밋.

후속 작업: Plan-B-Insta (services/insta-render + NAS insta 분할)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 01:31:47 +09:00

126 lines
4.0 KiB
Python

"""FastAPI app — Signal V2 Pull Worker."""
from __future__ import annotations
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from ai_trade import state as state_mod
from ai_trade.chronos_predictor import ChronosPredictor
from ai_trade.config import get_settings
from ai_trade.kis_client import KISClient
from ai_trade.kis_websocket import KISWebSocket
from ai_trade.pull_worker import poll_loop, make_asking_price_callback
from ai_trade.rate_limit import SignalDedup
from ai_trade.stock_client import StockClient
logger = logging.getLogger(__name__)
class AppContext:
client: StockClient | None = None
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
chronos: ChronosPredictor | None = None
_ctx = AppContext()
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
if not settings.webai_api_key:
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_ENV_TYPE=%s, KIS_%s_APP_KEY missing) — KIS REST/WebSocket disabled",
settings.kis_env_type, settings.kis_env_type.upper()
)
_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")
# Load Chronos (heavy: ~1GB model download first time)
try:
_ctx.chronos = ChronosPredictor(model_name=settings.chronos_model)
except Exception:
logger.exception("ChronosPredictor load failed — continuing without chronos predictions")
_ctx.poll_task = asyncio.create_task(
poll_loop(
_ctx.client, state_mod.state, _ctx.shutdown,
kis_client=_ctx.kis_client,
chronos=_ctx.chronos,
dedup=_ctx.dedup,
settings=settings,
)
)
yield
# Shutdown
if _ctx.shutdown is not None:
_ctx.shutdown.set()
if _ctx.poll_task is not None:
try:
await asyncio.wait_for(_ctx.poll_task, timeout=5.0)
except asyncio.TimeoutError:
_ctx.poll_task.cancel()
try:
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()
app = FastAPI(
title="Signal V2 Pull Worker", version="0.1.0", lifespan=lifespan
)
@app.get("/health")
async def health():
settings = get_settings()
return {
"status": "online",
"stock_api_url": settings.stock_api_url,
"last_poll": state_mod.state.last_updated,
"cache_size": _ctx.client.cache_size() if _ctx.client is not None else 0,
}