diff --git a/stock/app/conftest.py b/stock/app/conftest.py new file mode 100644 index 0000000..8d8de1f --- /dev/null +++ b/stock/app/conftest.py @@ -0,0 +1,15 @@ +"""Project-level pytest conftest. + +SP-A2: autouse fixture that resets all webai_cache TTLCaches between tests +so screener/portfolio/news cache state does not leak across test cases. +""" +import pytest +from app import webai_cache + + +@pytest.fixture(autouse=True) +def _reset_webai_cache(): + webai_cache.PORTFOLIO_CACHE.clear() + webai_cache.NEWS_CACHE.clear() + webai_cache.SCREENER_CACHE.clear() + yield diff --git a/stock/app/main.py b/stock/app/main.py index 1a11d66..3aab854 100644 --- a/stock/app/main.py +++ b/stock/app/main.py @@ -25,6 +25,7 @@ from .scraper import fetch_market_news, fetch_major_indices from .price_fetcher import get_current_prices, get_current_prices_detail from .ai_summarizer import summarize_news, OllamaError from .auth import verify_webai_key +from . import webai_cache app = FastAPI() @@ -418,8 +419,16 @@ def _augment_portfolio_with_pnl_pct(raw: dict) -> dict: @app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)]) def get_webai_portfolio(): - """web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가).""" - return _augment_portfolio_with_pnl_pct(get_portfolio()) + """web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가). + + SP-A2 server-side TTLCache 적용. V1+V2 동시 호출도 NAS에서 1회 계산. + """ + cached = webai_cache.cache_get_portfolio() + if cached is not None: + return cached + result = _augment_portfolio_with_pnl_pct(get_portfolio()) + webai_cache.cache_set_portfolio(result) + return result def _fetch_news_sentiment_dump(date: str | None) -> dict: @@ -466,8 +475,16 @@ def _fetch_news_sentiment_dump(date: str | None) -> dict: @app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)]) def get_webai_news_sentiment(date: str | None = None): - """web-ai 전용 news sentiment 일별 dump.""" - return _fetch_news_sentiment_dump(date) + """web-ai 전용 news sentiment 일별 dump. + + SP-A2 server-side TTLCache 적용. date 파라미터별로 별도 슬롯. + """ + cached = webai_cache.cache_get_news(date) + if cached is not None: + return cached + result = _fetch_news_sentiment_dump(date) + webai_cache.cache_set_news(date, result) + return result @app.post("/api/portfolio", status_code=201) diff --git a/stock/app/screener/router.py b/stock/app/screener/router.py index dc7cb67..14fdfd8 100644 --- a/stock/app/screener/router.py +++ b/stock/app/screener/router.py @@ -12,6 +12,7 @@ from fastapi import APIRouter, HTTPException from . import schemas from .registry import NODE_REGISTRY, GATE_REGISTRY +from .. import webai_cache router = APIRouter(prefix="/api/stock/screener") @@ -173,6 +174,12 @@ def _persist_run(conn, asof, mode, weights, node_params, gate_params, top_n, @router.post("/run", response_model=schemas.RunResponse) def post_run(body: schemas.RunRequest): from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR + # SP-A2 — preview 모드는 web-ai/frontend 폴링이라 캐시 적용. + # auto 모드는 실제 운영 트리거(휴장일 게이트 등)라 캐시 미적용. + if body.mode == "preview": + cached = webai_cache.cache_get_screener(body.mode, body.top_n, body.weights) + if cached is not None: + return cached started_at = dt.datetime.utcnow().isoformat() with _conn() as c: asof = _resolve_asof(body.asof, c) @@ -231,7 +238,7 @@ def post_run(body: schemas.RunRequest): top_n=top_n, rows=result.rows, run_id=run_id, ) - return schemas.RunResponse( + response = schemas.RunResponse( asof=asof.isoformat(), mode=body.mode, status="success", run_id=run_id, survivors_count=result.survivors_count, weights=weights, top_n=top_n, @@ -239,6 +246,10 @@ def post_run(body: schemas.RunRequest): telegram_payload=schemas.TelegramPayload(**payload), warnings=result.warnings, ) + # SP-A2 — preview 모드 결과 캐시 저장. + if body.mode == "preview": + webai_cache.cache_set_screener(body.mode, body.top_n, body.weights, response) + return response # ---------- /snapshot/refresh ----------