뉴스 시그널 TDD

Background

PRD 참조. 뉴스 감성 + 기술 추세를 종합한 참고 시그널을 별도 Python FastAPI 서비스로 제공한다. 코어(Kotlin)와 분리한 이유는 ADR-201 참조.

Overview

  • 서비스: signal-service (FastAPI, :8000). Python 3.11+, httpx, claude CLI.
  • 흐름: 토스 종목 판별 → 뉴스 수집 → 감성 → 캔들 추세 → 블렌딩 → 캐시.

Terminology

PRD를 따른다.

Define Problem

AS-IS

  • 시그널 인프라 없음. 토스엔 뉴스 없음.

TO-BE

  • 모듈형 파이프라인(news → sentiment → momentum → signal)으로 종합 점수 산출.

Possible Solutions

방안 비교

방안설명채택미채택 사유
별도 Python 서비스ML·뉴스 생태계 활용, FastAPI
Kotlin 코어에 통합단일 배포Python 뉴스·감성 도구 활용성↓, 코어 결합도↑
감성=외부 LLM APIANTHROPIC_API_KEY 직접 호출사용자 지시로 로그인된 claude -p CLI 사용(키 미사용)

Detail Design

종합 공식

composite = clamp(0.6 × sentiment + 0.4 × momentum, -1, 1)

label:
  ≥ 0.5      강한 긍정
  ≥ 0.15     긍정
  -0.15~0.15 중립
  > -0.5     부정
  ≤ -0.5     강한 부정

모멘텀 (3요소 평균)

요소계산정규화
MA20 이격(close − MA20)/MA20 ÷ 0.10±10% = ±1
MA5/MA20 크로스(MA5 − MA20)/MA20 ÷ 0.05 (데이터 ≥40)±5% = ±1
RSI14(RSI − 50)/50 (데이터 ≥15)50 중심 0

감성 (claude -p)

  1. 헤드라인 수집 (0건 → 즉시 중립).
  2. claude -p "<프롬프트>" (timeout 120s). 프롬프트 지침: 실적·수주·규제·신제품 가중, JSON만.
  3. {"score": -1~1, "label": ...} 파싱 → 클램핑. 파싱 불가 → NEUTRAL(0.0).

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant M as main.py
    participant Ca as TtlCache
    participant T as toss_client
    participant N as news
    participant S as sentiment(claude -p)
    participant Mo as momentum
    C->>M: GET /signals/{symbol}
    M->>Ca: get(symbol)
    alt 캐시 miss
        M->>T: get_stock / get_closes
        M->>N: fetch(headlines)
        M->>S: score(headlines)
        M->>Mo: score(closes)
        M->>M: combine(0.6:0.4) + classify
        M->>Ca: set(symbol)
    end
    M-->>C: Signal

응답 필드

symbol, name, market(KR/US), currency, sentiment_score, sentiment_label, momentum_score, composite_score, label, last_close, change_5d, headlines[], generatedAt.

ERD

별도 영속 스토리지 없음 (stateless + in-memory 캐시). watchlist는 BE(GET /api/v1/watchlist)에서 조회.

Testing Plan

  • signal.py: 0.6:0.4 공식 검증, 5단계 라벨 임계값 경계값 확인, [-1,1] 클램핑 동작 검증.
  • sentiment.py: 정상 JSON 파싱, 파싱 불가·타임아웃·헤드라인 0건 시 NEUTRAL(0.0) 폴백 검증.
  • momentum.py: 3요소 정상 계산, 데이터 부족(MA20/크로스/RSI 누락) 처리, 클램핑 동작.
  • cache.py: hit 시 재계산 미발생 확인, TTL 만료 후 miss 전환, 종목 키 독립성.
  • toss_client.py: 정상 토큰 발급, 401 재발급 후 재시도 성공, 연속 실패 예외 전파.
  • 외부 의존(claude CLI, 토스 API, 뉴스 RSS)은 모두 unittest.mock.patch로 처리한다.

Release Scenario

  1. cd ml && uv run uvicorn app.main:app --port 8000.
  2. 환경변수(TOSS_API_KEY/SECRET, BACKEND_BASE_URL, SIGNAL_CACHE_TTL, NEWS_LIMIT) 주입.
  3. 전제: 로그인된 claude CLI.
  4. 롤백: ML 서비스 중단 시 FE 시그널 탭만 비활성, BE/알림은 독립 동작.

Observability

관측 지표

지표측정 방법임계값
claude -p 호출 시간subprocess 실행 시간 로그>10s 경고
감성 파싱 실패율NEUTRAL 폴백 횟수 / 전체 호출>20% 경고
캐시 hit/miss요청별 로그 (INFO: cache hit <symbol>)
뉴스 수집 실패빈 헤드라인 반환 횟수
API 응답 시간콜드 vs. hit 비교콜드 >10s 경고

알람 조건

  • claude -p timeout (120s) 발생 시 WARNING 레벨 로그.
  • 토스 API 401 재발급 3회 연속 실패 시 ERROR 레벨 로그.
  • GDELT + Google 뉴스 모두 실패(빈 헤드라인) 시 WARNING 레벨 로그.

로그 포인트

  • sentiment.py: 호출 시작·완료·실패(파싱 오류/타임아웃)
  • toss_client.py: 401 재발급 시도·성공·실패
  • news.py: GDELT 폴백 발생
  • cache.py: hit(cache hit {symbol})·miss(cache miss {symbol})·만료(cache expired {symbol})

FE 영향 분석

  • FE 시그널 탭은 ML(:8000) 직접 호출(NEXT_PUBLIC_SIGNAL_BASE_URL), BFF 미경유.
  • watchlist는 BE(:8080) 호출로 분리.

Project Information

항목내용
프로젝트주식앱 — 뉴스 시그널
담당자1인 (biuea)
서비스명signal-service (FastAPI, :8000)
스택Python 3.11+, FastAPI, httpx, uv, claude CLI
대상 티켓STK2-01 ~ STK2-08

Document History

날짜변경 내용작성자
2026-06-19초안 작성 (Background~Release Scenario)biuea
2026-06-19Observability 상세화, Project Information·Document History 추가biuea
2026-06-21스킬 포맷 정비 (Testing Plan bullet 변환, Phase 표현 제거)biuea

관련 문서