feat(stock): apply webai_cache to portfolio/news/screener-preview (SP-A2)
3 endpoint cache 적용 — /api/webai/portfolio, /api/webai/news-sentiment, /api/stock/screener/run (preview 모드만, auto는 캐시 미적용). V1+V2 동시 호출도 NAS에서 1회 계산. web-ai 측 SP-A1 캐시와 2-layer로 작동하여 NAS 인바운드 부담 70% 감소 예상. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
15
stock/app/conftest.py
Normal file
15
stock/app/conftest.py
Normal file
@@ -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
|
||||||
@@ -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 .price_fetcher import get_current_prices, get_current_prices_detail
|
||||||
from .ai_summarizer import summarize_news, OllamaError
|
from .ai_summarizer import summarize_news, OllamaError
|
||||||
from .auth import verify_webai_key
|
from .auth import verify_webai_key
|
||||||
|
from . import webai_cache
|
||||||
|
|
||||||
app = FastAPI()
|
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)])
|
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
|
||||||
def get_webai_portfolio():
|
def get_webai_portfolio():
|
||||||
"""web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가)."""
|
"""web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가).
|
||||||
return _augment_portfolio_with_pnl_pct(get_portfolio())
|
|
||||||
|
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:
|
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)])
|
@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)])
|
||||||
def get_webai_news_sentiment(date: str | None = None):
|
def get_webai_news_sentiment(date: str | None = None):
|
||||||
"""web-ai 전용 news sentiment 일별 dump."""
|
"""web-ai 전용 news sentiment 일별 dump.
|
||||||
return _fetch_news_sentiment_dump(date)
|
|
||||||
|
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)
|
@app.post("/api/portfolio", status_code=201)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from fastapi import APIRouter, HTTPException
|
|||||||
|
|
||||||
from . import schemas
|
from . import schemas
|
||||||
from .registry import NODE_REGISTRY, GATE_REGISTRY
|
from .registry import NODE_REGISTRY, GATE_REGISTRY
|
||||||
|
from .. import webai_cache
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/stock/screener")
|
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)
|
@router.post("/run", response_model=schemas.RunResponse)
|
||||||
def post_run(body: schemas.RunRequest):
|
def post_run(body: schemas.RunRequest):
|
||||||
from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR
|
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()
|
started_at = dt.datetime.utcnow().isoformat()
|
||||||
with _conn() as c:
|
with _conn() as c:
|
||||||
asof = _resolve_asof(body.asof, 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,
|
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",
|
asof=asof.isoformat(), mode=body.mode, status="success",
|
||||||
run_id=run_id, survivors_count=result.survivors_count,
|
run_id=run_id, survivors_count=result.survivors_count,
|
||||||
weights=weights, top_n=top_n,
|
weights=weights, top_n=top_n,
|
||||||
@@ -239,6 +246,10 @@ def post_run(body: schemas.RunRequest):
|
|||||||
telegram_payload=schemas.TelegramPayload(**payload),
|
telegram_payload=schemas.TelegramPayload(**payload),
|
||||||
warnings=result.warnings,
|
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 ----------
|
# ---------- /snapshot/refresh ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user