Files
web-page-backend/stock/app/webai_cache.py
gahusb 030365bed0 feat(stock): webai_cache module (TTLCache for SP-A2)
3개의 TTLCache (portfolio 120s · news 600s · screener 180s) +
헬퍼 함수. screener key는 mode + top_n + weights canonical hash로
분기. 다음 커밋에서 /api/webai/portfolio·news-sentiment·screener/run
3 endpoint에 적용.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:43:24 +09:00

69 lines
2.1 KiB
Python

"""SP-A2 — NAS stock의 /api/webai/* 엔드포인트 in-memory TTLCache.
web-ai 측 캐시(stock_client._TTL)가 miss됐을 때도 NAS에서 같은 데이터를
KIS·LLM 재호출 없이 즉시 반환하기 위한 2-layer 캐시의 server 측.
V1+V2가 동시 호출해도 NAS는 1회만 계산.
TTL 정책 (spec §10 SP-A2):
- portfolio: 120s (web-ai TTL 180s 보다 짧게 — 변경 감지 가능)
- news: 600s (sentiment는 일 단위)
- screener: 180s
"""
from __future__ import annotations
import hashlib
import json
from typing import Any, Optional
from cachetools import TTLCache
PORTFOLIO_CACHE: TTLCache = TTLCache(maxsize=1, ttl=120.0)
NEWS_CACHE: TTLCache = TTLCache(maxsize=10, ttl=600.0)
SCREENER_CACHE: TTLCache = TTLCache(maxsize=10, ttl=180.0)
# ----- portfolio -----
def cache_get_portfolio() -> Optional[Any]:
return PORTFOLIO_CACHE.get("result")
def cache_set_portfolio(value: Any) -> None:
PORTFOLIO_CACHE["result"] = value
# ----- news-sentiment -----
def _news_key(date: Optional[str]) -> str:
return date if date else "latest"
def cache_get_news(date: Optional[str]) -> Optional[Any]:
return NEWS_CACHE.get(_news_key(date))
def cache_set_news(date: Optional[str], value: Any) -> None:
NEWS_CACHE[_news_key(date)] = value
# ----- screener -----
def _screener_key(mode: str, top_n: int, weights: Optional[dict]) -> str:
"""mode + top_n + weights canonical hash. weights 객체 동등성을 키로."""
if weights is None:
w_repr = "none"
else:
# canonical: sorted keys → md5 hex (긴 weights도 짧은 키로)
canon = json.dumps(weights, sort_keys=True, ensure_ascii=False)
w_repr = hashlib.md5(canon.encode("utf-8")).hexdigest()[:12]
return f"{mode}:{top_n}:{w_repr}"
def cache_get_screener(mode: str, top_n: int, weights: Optional[dict]) -> Optional[Any]:
return SCREENER_CACHE.get(_screener_key(mode, top_n, weights))
def cache_set_screener(mode: str, top_n: int, weights: Optional[dict], value: Any) -> None:
SCREENER_CACHE[_screener_key(mode, top_n, weights)] = value