TDD — 관심종목 고도화

Background

stock-application 모노레포. BE = Kotlin/Spring(:8080), ML = Python FastAPI(:8000), FE = Next.js(:3000). 관심종목 뉴스 시그널을 구축했으나 헤드라인이 제목 문자열만 반환된다. 뉴스 링크·번역·per-headline 감성·요약·예측 차트를 추가해 카드 정보 밀도를 높인다.

Overview

4개 변경 축:

  1. ML 뉴스 파이프라인 고도화: Headline 데이터 클래스(제목·URL·번역제목·감성 레이블) 반환, 전체 요약 추가
  2. ML 예측 API: 기술적 지표 기반 단기(5일) 예측 곡선 엔드포인트
  3. BE 뉴스 스냅샷 영속화: news_snapshots 테이블, 조회 API
  4. FE UI 개선: 감성 레이블 직관화, 뉴스 링크+배지, 세부 페이지+차트

Terminology

용어설명
Headline뉴스 항목 1건 — 제목·URL·번역 제목·호악재 레이블 포함
뉴스 스냅샷특정 시점 심볼의 뉴스 헤드라인 목록 + 요약 — BE DB에 영속화
예측 곡선기술적 모멘텀(MA·RSI) 기반 단기 가격 방향 참고 시그널
호재/악재 레이블개별 헤드라인의 투자 방향성 분류
행동 레이블종합 시그널을 “매수 유망/상승 주목/중립 관망/하락 주의/매도 주의”로 표현

Define Problem

AS-IS

Signal.headlines: list[str]  # 제목만, URL 없음
signal.label: "강한 긍정" | "긍정" | "중립" | "부정" | "강한 부정"  # 행동 의미 불명확
뉴스 요약: 없음
해외 뉴스: 영어 그대로 노출
per-headline 감성: 없음
예측 차트: 없음 (백테스트 요약만 있음)

TO-BE

Signal.headlines: list[Headline]  # {title, url, translated_title, sentiment}
Signal.news_summary: str  # "전반적으로 수익 개선 기대감이 높고 공급망 우려는 제한적"
signal.label: "매수 유망" | "상승 주목" | "중립 관망" | "하락 주의" | "매도 주의"
GET /predictions/{symbol} → {historical, predicted, note}
BE: news_snapshots 테이블 영속화
FE: 세부 페이지 /watchlist/[symbol]

Possible Solutions

방안 비교

방안설명왜 채택미채택 대안
per-headline 감성 (클로드 1회 배치)전체 헤드라인을 claude -p 한 번 호출로 per-item 분류 + 요약 동시 추출기존 claude -p 패턴 재사용, API 호출 1회각 헤드라인별 개별 호출(느림·비용 多)
번역 (클로드 통합)per-headline 감성 프롬프트에 번역 요청 포함추가 API 호출 없이 처리Google Translate API(유료)
예측 곡선 (선형회귀 + 변동성 밴드)최근 20일 종가 → 선형회귀 기울기 + 모멘텀 방향 가중 → 5일 예측, std로 밴드외부 ML 라이브러리 불필요, 결정론적, 설명 가능LSTM(복잡도 높음·오버피팅 위험)
BE 뉴스 영속화 (Watchlist 도메인 추가)news_snapshots 테이블, WatchlistItem과 1:1, TTL 기반 stale 여부기존 watchlist 도메인 확장, 심플별도 서비스 분리(불필요한 복잡도)

Detail Design

감성 레이블 매핑

기존변경composite 범위
강한 긍정매수 유망 🟢≥ 0.5
긍정상승 주목≥ 0.15
중립중립 관망> -0.15
부정하락 주의> -0.5
강한 부정매도 주의 🔴< -0.5

Component Diagram

flowchart LR
    subgraph FE["FE (Next.js)"]
        WIC["WatchlistItemCard\n(클릭→상세)"]
        DP["watchlist/[symbol]\n(세부 페이지)"]
        PC["PredictionChart"]
        NI["NewsItem\n(배지+링크)"]
    end
    subgraph BE["BE (:8080)"]
        WAC["WatchlistApiController"]
        GNSU["GetNewsSnapshotUseCase"]
        SNSU["SaveNewsSnapshotUseCase"]
        NSDS["NewsSnapshotDomainService"]
        NSR["NewsSnapshotRepository\n(interface)"]
        NSRI["NewsSnapshotRepositoryImpl"]
    end
    subgraph ML["ML (:8000)"]
        SIG["GET /signals/{symbol}"]
        PRED["GET /predictions/{symbol}"]
        AH2["analyze_headlines_v2\n(claude -p)"]
        BP["build_prediction\n(선형회귀)"]
    end
    WIC --> DP
    DP --> SIG
    DP --> PRED
    DP --> WAC
    SIG --> AH2
    PRED --> BP
    WAC --> GNSU
    WAC --> SNSU
    GNSU --> NSDS
    SNSU --> NSDS
    NSDS --> NSR
    NSR -.->|implements| NSRI

Sequence Diagram — 세부 페이지 로드

sequenceDiagram
    participant U as User
    participant FE
    participant BE as BE :8080
    participant ML as ML :8000
    U->>FE: 관심종목 클릭
    FE->>BE: GET /api/v1/watchlist/{symbol}/news
    FE->>ML: GET /signals/{symbol}
    FE->>ML: GET /predictions/{symbol}
    BE-->>FE: NewsSnapshot (요약+헤드라인)
    ML-->>FE: Signal (행동 레이블)
    ML-->>FE: Prediction (historical+predicted)
    FE->>U: 차트 + 뉴스 요약 + 시그널 렌더

예측 곡선 알고리즘

입력: closes[N] (최근 20일 종가)
1. 선형회귀: slope = (closes[-1] - closes[-5]) / 5  # 5일 기울기
2. 모멘텀 가중: adjusted_slope = slope * (1 + momentum_score)
3. 예측가: p[d] = closes[-1] + adjusted_slope * d  (d=1..5)
4. 밴드: σ = std(closes[-20:])
         upper[d] = p[d] + 1.5 * σ
         lower[d] = p[d] - 1.5 * σ
주의문구: "기술적 모멘텀 기반 참고 시그널 — 투자 판단 근거로 사용하지 마십시오"

ML claude -p 통합 프롬프트 (analyze_headlines_v2)

다음은 {market} 종목의 최근 뉴스 목록이다(URL·제목 쌍).
각 헤드라인에 대해:
  1. 한국어 번역 제목 (KR이면 원문 유지)
  2. 호악재 분류: "호재" | "악재" | "중립"
그리고 전체 헤드라인 기반 1줄 한국어 요약.
JSON만 답하라. 형식:
{
  "summary": "...",
  "items": [{"title": "원문", "translated_title": "번역", "sentiment": "호재|악재|중립"}]
}

ERD

erDiagram
    watchlist_items ||--o| news_snapshots : has
    watchlist_items {
        bigint id PK
        varchar symbol
    }
    news_snapshots {
        bigint id PK
        varchar symbol UK
        text summary
        json headlines
        datetime(6) refreshed_at
        datetime(6) created_at
    }

headlines 컬럼 JSON 구조:

[{"title":"...", "url":"...", "translated_title":"...", "sentiment":"호재"}]

Testing Plan

  • ML: analyze_headlines_v2 프롬프트 파싱 및 JSON 응답 구조 검증 (pytest + mock)
  • ML: build_prediction 수치 검증 — 방향 일치·밴드 포함 여부·빈 입력 예외 (pytest)
  • BE domain: NewsSnapshotDomainService.isStale 1시간 경계 판단 (Kotest BehaviorSpec + MockK)
  • BE application: GetNewsSnapshotUseCase 신선/stale 분기·ML 실패 시 graceful degradation (Kotest + MockK)
  • BE infrastructure: NewsSnapshotRepositoryImpl symbol 기준 upsert (Kotest + Testcontainers MySQL)
  • BE presentation: GET /api/v1/watchlist/{symbol}/news 응답 구조·404 케이스 (Kotest + MockMvc)
  • FE: PredictionChart 실선/점선/밴드 렌더, NewsItem 배지·링크, SignalLabel 5단계 매핑 (Vitest)

Release Scenario

  1. ML 배포: analyze_headlines_v2 적용 (기존 헤드라인 타입 변경 — FE 배포 전 호환 필요)
  2. BE 배포: Flyway 마이그레이션 후 배포
  3. FE 배포: 새 Signal 타입 + 세부 페이지

롤백: ML Signal 응답 shape 변경이 Breaking Change이므로 FE 배포 전까지 /signals v1 응답 유지 또는 FE와 동시 배포.

Project Information

  • 담당: biuea
  • 기간: ~1주

Document History

날짜변경 내용작성자
2026-06-19초안 작성Claude
2026-06-21스킬 포맷 정렬Claude