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>
69 lines
2.1 KiB
Python
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
|