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>
This commit is contained in:
68
stock/app/webai_cache.py
Normal file
68
stock/app/webai_cache.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user