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:
@@ -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,
|
||||
|
||||
@@ -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]] = []
|
||||
|
||||
@@ -125,6 +125,16 @@ from . import telegram as _tg
|
||||
from .engine import Screener, ScreenContext
|
||||
|
||||
|
||||
def _today_kst() -> dt.date:
|
||||
"""KST 오늘 날짜.
|
||||
|
||||
stock 컨테이너는 python:3.12-alpine + tzdata 미설치라 TZ=Asia/Seoul이 무효 →
|
||||
date.today()가 UTC를 반환한다. 08시대(KST) 리포트가 하루 밀리는 것을 막기 위해
|
||||
UTC+9로 명시 보정한다(holdings_intel._today_kst와 동일한 관용).
|
||||
"""
|
||||
return (dt.datetime.utcnow() + dt.timedelta(hours=9)).date()
|
||||
|
||||
|
||||
def _resolve_asof(asof_str, conn: sqlite3.Connection) -> dt.date:
|
||||
if asof_str:
|
||||
return dt.date.fromisoformat(asof_str)
|
||||
@@ -263,7 +273,7 @@ from . import snapshot as _snap
|
||||
|
||||
@router.post("/snapshot/refresh")
|
||||
def post_snapshot_refresh(asof: Optional[str] = None):
|
||||
asof_date = dt.date.fromisoformat(asof) if asof else dt.date.today()
|
||||
asof_date = dt.date.fromisoformat(asof) if asof else _today_kst()
|
||||
if asof_date.weekday() >= 5:
|
||||
return {"asof": asof_date.isoformat(), "status": "skipped_weekend"}
|
||||
with _conn() as c:
|
||||
@@ -300,7 +310,7 @@ from .ai_news import validation as _ai_validation
|
||||
|
||||
@router.post("/snapshot/refresh-news-sentiment")
|
||||
async def post_refresh_news_sentiment(asof: Optional[str] = None):
|
||||
asof_date = dt.date.fromisoformat(asof) if asof else dt.date.today()
|
||||
asof_date = dt.date.fromisoformat(asof) if asof else _today_kst()
|
||||
if asof_date.weekday() >= 5:
|
||||
return {"asof": asof_date.isoformat(), "status": "skipped_weekend"}
|
||||
if _is_holiday(asof_date):
|
||||
|
||||
Reference in New Issue
Block a user