fix(stock): AI 뉴스 리포트 하루 밀림 해소 — asof를 KST로 보정 + LLM에 현재 일자 주입

근본원인: stock 컨테이너는 python:3.12-alpine + tzdata 미설치라 TZ=Asia/Seoul이
무효 → date.today()가 UTC를 반환. AI 뉴스 리포트 cron은 08:00 KST(=전날 23:00 UTC)라
asof가 어제로 계산돼 라벨·기사 윈도우·news_sentiment 저장이 전부 하루 밀렸음
(월요일은 일요일 UTC로 계산돼 skip_weekend까지).

- screener/router.py: _today_kst()(=utcnow+9h, holdings_intel 관용) 추가.
  /snapshot/refresh · /snapshot/refresh-news-sentiment의 asof 기본값을 KST로.
- ai_news/analyzer.py: score_sentiment(asof=...) → 프롬프트 앞에 "오늘 날짜" 명시,
  LLM이 현재 일자 기준으로 뉴스 평가(사용자 요청).
- ai_news/pipeline.py: refresh_daily가 asof를 score_sentiment까지 스레딩.
- 테스트: _today_kst KST 보정 + analyzer asof 주입 2종 TDD Red→Green.
  기존 pipeline 목 시그니처에 asof 반영. stock 전체 149 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
This commit is contained in:
2026-07-02 14:38:51 +09:00
parent db6fed72b3
commit 36e8d11060
6 changed files with 55 additions and 11 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import datetime as dt
import json
import logging
import os
@@ -59,13 +60,19 @@ async def score_sentiment(
*,
name: str | None = None,
model: str = DEFAULT_MODEL,
asof: dt.date | None = None,
) -> Dict[str, Any]:
"""Returns {ticker, score_raw, reason, news_count, tokens_input, tokens_output, model}."""
"""Returns {ticker, score_raw, reason, news_count, tokens_input, tokens_output, model}.
asof(현재 KST 일자)를 주면 prompt 맨 앞에 오늘 날짜를 명시해 LLM이 현재 시점 기준으로 판단한다.
"""
news_block = _format_news_block(news)
prompt = PROMPT_TEMPLATE.format(
name=name or ticker, ticker=ticker,
n=len(news), news_block=news_block,
)
if asof is not None:
prompt = f"오늘 날짜: {asof.isoformat()} (이 시점 기준으로 뉴스를 평가하세요)\n\n" + prompt
resp = await llm.messages.create(
model=model,
max_tokens=200,

View File

@@ -39,11 +39,11 @@ def _make_llm():
async def _process_one(
ticker: str, name: str, articles: List[Dict[str, Any]],
sem: asyncio.Semaphore, llm, model: str,
sem: asyncio.Semaphore, llm, model: str, asof: dt.date,
) -> Dict[str, Any]:
async with sem:
return await _analyzer.score_sentiment(
llm, ticker, articles, name=name, model=model,
llm, ticker, articles, name=name, model=model, asof=asof,
)
@@ -110,7 +110,7 @@ async def refresh_daily(
arts = articles_by_ticker.get(t, [])
if not arts:
continue # 매핑 0 — score 미생성
tasks.append(_process_one(t, name_map.get(t, t), arts, sem, llm, model))
tasks.append(_process_one(t, name_map.get(t, t), arts, sem, llm, model, asof))
raw_results = await asyncio.gather(*tasks, return_exceptions=True)
successes: List[Dict[str, Any]] = []