- backend/app/nlp/finbert.py: snunlp/KR-FinBert-SC 어댑터. - score = P(pos) - P(neg) ∈ [-1, +1], label = argmax (neg/neu/pos) - 768d mean-pooled last hidden state → news.embedding (VECTOR) 저장 - settings.huggingface_token 인증, lazy singleton, cuda/cpu auto - backend/app/nlp/score_news.py: news 테이블에서 sentiment_score IS NULL 행을 배치 스코어 → UPDATE (... embedding=(:e)::vector). 종목 필터 + limit 옵션. - backend/app/db/migrations/002_sentiment_view.sql: v_sentiment_daily 뷰. 종목·KST 일별 n_articles, mean_score, pos/neg/neu_ratio, weighted_score (naver_finance 1.0 / google_rss 0.7 / dart 0.5). - backend/app/db/migrate.py: 이미 실행 중인 DB 에 새 SQL 마이그레이션 적용용 CLI. 모든 SQL 파일은 idempotent. - refresh_one.py: refresh 끝에 종목당 200건까지 finbert 스코어, finbert SourceStatus 를 RefreshReport 에 추가. - daily_batch.py: 모든 종목 처리 후 score_pending_news(limit=2000) 로 mop-up. 모델 캐시는 docker-compose hf_cache 볼륨(/root/.cache/huggingface). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
54 lines
1.7 KiB
Python
54 lines
1.7 KiB
Python
"""Manual migration runner.
|
|
|
|
docker-entrypoint-initdb.d 는 fresh DB 첫 기동 때만 동작. 이미 동작 중인 DB 에
|
|
새 마이그레이션을 적용하려면 이 스크립트로:
|
|
|
|
python -m app.db.migrate
|
|
|
|
모든 SQL 파일은 idempotent (CREATE IF NOT EXISTS / CREATE OR REPLACE) 여야 함.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import text
|
|
|
|
from app.db.connection import get_engine
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MIGRATIONS_DIR = Path(__file__).parent / "migrations"
|
|
|
|
|
|
def apply_all() -> dict[str, str]:
|
|
"""migrations/ 안 .sql 들을 이름순으로 적용. 결과: {filename: 'ok'|'failed: ...'}."""
|
|
eng = get_engine()
|
|
results: dict[str, str] = {}
|
|
files = sorted(MIGRATIONS_DIR.glob("*.sql"))
|
|
if not files:
|
|
logger.warning("no migration files in %s", MIGRATIONS_DIR)
|
|
return results
|
|
for path in files:
|
|
sql = path.read_text(encoding="utf-8")
|
|
# psql meta-command 제거 (\set ON_ERROR_STOP 등)
|
|
cleaned = "\n".join(
|
|
ln for ln in sql.splitlines() if not ln.strip().startswith("\\")
|
|
)
|
|
try:
|
|
with eng.begin() as conn:
|
|
conn.execute(text(cleaned))
|
|
results[path.name] = "ok"
|
|
logger.info("applied %s", path.name)
|
|
except Exception as exc: # noqa: BLE001
|
|
results[path.name] = f"failed: {exc}"
|
|
logger.exception("migration %s failed", path.name)
|
|
return results
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
out = apply_all()
|
|
for k, v in out.items():
|
|
print(f"{k}: {v}")
|