Files
web-page/docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md
gahusb 094366a162 docs(signal-v2): Phase 1 stock WebAI API spec
3 endpoints + X-WebAI-Key auth + nginx rate limit + 15 tests.
brainstorming 7 decisions: scope=B / auth=A(static key) / portfolio=B(pnl_pct) /
news-sentiment=A(daily dump) / endpoint=1(/api/webai prefix) / rate=B(nginx) /
test=B(pytest schema).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:24:37 +09:00

15 KiB
Raw Blame History

Confidence Signal Pipeline V2 — Phase 1: stock WebAI API Design

작성일: 2026-05-15 작성자: gahusb 상태: Approved for implementation 선행 spec:

  • Phase 0 architecture (2026-05-15-confidence-signal-pipeline-v2-architecture.md)
  • stock-lab → stock graduation (2026-05-15-stock-lab-rename-to-stock.md) — 본 spec 부터 새 이름 stock 사용 브레인스토밍 결정 7개: scope=B / auth=A(정적키) / portfolio shape=B(pnl_pct 추가) / news-sentiment=A(일별 dump) / endpoint 구조=1(/api/webai 분리) / rate limit=B(nginx + 인증 로그) / 테스트=B(pytest schema 검증)

1. 목표

Confidence Signal Pipeline V2 의 Phase 2 (web-ai pull worker) 가 stock 컨테이너에서 polling 으로 가져갈 입력 계약 3종을 stock 측에 신설.

stock 의 가치 발굴 데이터 (portfolio, news sentiment, screener 점수) 를 web-ai 가 안전하게 polling 할 수 있는 인증된 endpoint 묶음 = Phase 2 진입 전 필수 의존성.

Why: Phase 0 §3 책임 분리 — "stock = 가치 발굴, web-ai = 시점 분석". web-ai 가 NAS DB 직접 접근 안 함, 모든 데이터는 stock API 경유. 본 Phase 가 이 API 표면을 정의.


2. 범위

포함 (Phase 1)

  • ① 새 endpoint GET /api/webai/portfolio — 기존 portfolio 응답 + pnl_pct 필드 보강 + X-WebAI-Key 인증
  • ② 새 endpoint GET /api/webai/news-sentiment — news_sentiment 테이블 일별 dump + 인증
  • ③ X-WebAI-Key 인증 인프라 — verify_webai_key FastAPI dependency, env WEBAI_API_KEY
  • ④ nginx /api/webai/* location + limit_req rate limit (분당 60 + burst 20)
  • ⑤ 인증 실패 logger (path + remote_addr 1회 기록)
  • ⑥ 단위 + 통합 테스트 15 케이스

범위 외 (NOT)

  • /api/webai/screener/run 신규 endpoint 불필요 — web-ai 는 기존 /api/stock/screener/run {mode:"preview"} 직접 호출 (Phase 2 client 구현 시 동작 검증)
  • 기존 /api/portfolio 의 무인증 외부 노출 보안 강화 — 별도 슬라이스 (사용자 인증 도입은 Lab 사이트 통합 로그인 검토 시점)
  • portfolio 의 entry_date / days_held / position_weight 등 추가 필드 — backlog (V2 운영 후 sell signal 정밀화 시)
  • HMAC 서명, mTLS, IP allowlist — 단일 클라이언트 시나리오 + 정적 키로 충분
  • nginx rate limit 응답 시간/에러율 메트릭 + 알림 — Phase 7 운영 모니터링 슬라이스
  • 운영 .env 변경 자동화 — 사용자 1회 수동 갱신
  • web-ui 변경 — Phase 1 은 백엔드 + 인프라만

3. 변경 매트릭스

3.1 web-backend 코드

파일 변경
stock/app/auth.py (신규) verify_webai_key() FastAPI dependency
stock/app/main.py 신규 endpoint 2개: GET /api/webai/portfolio, GET /api/webai/news-sentiment (둘 다 dependencies=[Depends(verify_webai_key)]). portfolio 는 기존 get_portfolio() 호출 + pnl_pct 보강 mapper
stock/app/test_webai_auth.py (신규) verify_webai_key 단위 3 케이스
stock/app/test_webai_endpoints.py (신규) 두 endpoint × 4 케이스 + 공통 4 케이스 = 12 케이스
nginx/default.conf limit_req_zone webai 정의 + /api/webai/ location + X-WebAI-Key 헤더 forward
docker-compose.yml stock 의 env 에 WEBAI_API_KEY=${WEBAI_API_KEY} 추가

3.2 운영 (사용자 1회)

파일 변경
운영 .env (NAS /volume1/docker/webpage/.env) WEBAI_API_KEY=<랜덤 32~64자> 추가
Windows web-ai 의 .env WEBAI_API_KEY=<동일 값> 추가 (Phase 2 진입 시점에 사용)

3.3 web-ui

변경 없음. 기존 /api/portfolio 호출 무영향.


4. API 계약

4.1 GET /api/webai/portfolio

요청:

GET /api/webai/portfolio HTTP/1.1
X-WebAI-Key: <key>

응답 200 — 기존 /api/portfolio 응답 + 각 holdings 항목에 pnl_pct (비율) 추가 + summary 에 total_pnl_pct 추가:

{
  "holdings": [
    {
      "id": 1, "broker": "키움", "ticker": "005930", "name": "삼성전자",
      "quantity": 100, "avg_price": 75000, "purchase_price": 75500,
      "current_price": 78500, "price_session": "REGULAR",
      "price_as_of": "2026-05-15T15:30:00",
      "eval_amount": 7850000, "profit_amount": 350000,
      "profit_rate": 4.67,
      "pnl_pct": 0.0467
    }
  ],
  "cash": [{"broker": "키움", "cash": 1000000}],
  "summary": {
    "total_buy": 7550000, "total_eval": 7850000,
    "total_profit": 350000, "total_profit_rate": 4.67, "total_pnl_pct": 0.0467,
    "total_cash": 1000000, "total_assets": 8850000
  }
}

규칙:

  • pnl_pct = profit_rate / 100
  • 빈 portfolio 시 응답은 {"holdings": [], "cash": [...], "summary": {..., "total_pnl_pct": 0.0}}
  • profit_rate 가 null 인 holding (현재가 조회 실패) 의 pnl_pct 도 null

4.2 GET /api/webai/news-sentiment?date=YYYY-MM-DD

요청:

GET /api/webai/news-sentiment HTTP/1.1
X-WebAI-Key: <key>

쿼리:

  • date (옵션) — YYYY-MM-DD. 생략 시 news_sentiment 테이블의 최신 date.

응답 200:

{
  "date": "2026-05-15",
  "count": 87,
  "items": [
    {"ticker": "005930", "name": "삼성전자", "score": 6.2,
     "reason": "HBM 양산 가시화", "news_count": 12, "source": "articles"},
    {"ticker": "000660", "name": "SK하이닉스", "score": 5.5,
     "reason": "...", "news_count": 8, "source": "articles"}
  ]
}

규칙:

  • score = news_sentiment.score_raw 그대로 (단위 -10 ~ +10 가정, ai_news/analyzer.py 결정)
  • name = krx_master JOIN (없으면 ticker 그대로)
  • source = 디버그용 (articles / scraper / etc.)
  • 정렬 = score DESC (web-ai 가 자체 필터링)
  • 테이블 empty 또는 지정 date 데이터 없음 → {"date": null, "count": 0, "items": []}

4.3 인증 실패 (모든 /api/webai/* 공통)

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"detail": "invalid or missing X-WebAI-Key"}
  • 페이로드 leak 없음 (응답에 endpoint 별 데이터 0)
  • stock logger 에 WARNING auth_fail path=/api/webai/portfolio remote=1.2.3.4 1회 기록 (IP 만, 키는 로그하지 않음)

4.4 운영 .env 누락 시

env WEBAI_API_KEY 가 빈 문자열 또는 미정의 시:

  • startup 시점에 stock logger 가 ERROR WEBAI_API_KEY not configured 1회 출력
  • /api/webai/* 호출은 모두 503 {"detail": "webai auth not configured"}
  • 다른 endpoint (/api/portfolio, /api/stock/*) 영향 없음

5. 인증 구현

stock/app/auth.py:

import os
import logging
from fastapi import Header, HTTPException, Request

logger = logging.getLogger(__name__)
_WEBAI_API_KEY = os.getenv("WEBAI_API_KEY", "").strip()

def verify_webai_key(
    request: Request,
    x_webai_key: str | None = Header(default=None, alias="X-WebAI-Key"),
):
    if not _WEBAI_API_KEY:
        logger.error("WEBAI_API_KEY not configured — refusing all /api/webai/* requests")
        raise HTTPException(status_code=503, detail="webai auth not configured")
    if not x_webai_key or x_webai_key != _WEBAI_API_KEY:
        logger.warning(
            "auth_fail path=%s remote=%s",
            request.url.path,
            request.client.host if request.client else "?",
        )
        raise HTTPException(status_code=401, detail="invalid or missing X-WebAI-Key")

디자인 노트:

  • env 누락 시 import-time crash 회피 → 다른 endpoint 무영향. 호출 시점에만 503.
  • 키 비교는 == (constant-time 비교 불필요 — 단일 정적 키, timing attack 가치 낮음, 회전 후 즉시 무효화 가능).
  • 헤더 이름은 alias X-WebAI-Key (FastAPI 가 x_webai_key 매개변수로 받음).

stock/app/main.py 적용:

from .auth import verify_webai_key

@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
def get_webai_portfolio():
    raw = get_portfolio()  # 기존 함수 그대로 호출 (내부 분리: 응답 dict 생성 로직을 함수로)
    return _augment_portfolio_with_pnl_pct(raw)

@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)])
def get_webai_news_sentiment(date: str | None = None):
    return _fetch_news_sentiment_dump(date)

6. nginx config

web-backend/nginx/default.conf 변경:

6.1 http {} 블록 상단 (기존 limit_req_zone 옆에 추가)

limit_req_zone $binary_remote_addr zone=webai:5m rate=60r/m;

6.2 server {} 블록 내 신규 location (/api/stock/ location 위에 우선순위)

location /api/webai/ {
    limit_req zone=webai burst=20 nodelay;
    limit_req_status 429;

    proxy_pass http://stock:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-WebAI-Key $http_x_webai_key;
}

디자인 노트:

  • 60r/m = 분당 60 요청, burst=20 nodelay = 짧은 spike 20 까지 허용.
  • web-ai 폴링 빈도 (장중 분당 3 call) 대비 20배 여유 — 정상 운영 시 절대 hit 안 됨.
  • 한도 초과 시 429. web-ai 측 retry/backoff 는 Phase 2 client 구현 (본 Phase 외).
  • X-WebAI-Key 헤더 명시적 forward (nginx 가 underscore 헤더를 기본 drop 하므로 dash 헤더는 OK, 그래도 안전상 명시).

7. 테스트

7.1 단위 (stock/app/test_webai_auth.py, 3 케이스)

케이스 검증
test_verify_with_valid_key_passes WEBAI_API_KEY=secret + 헤더 X-WebAI-Key: secret → 통과
test_verify_without_key_raises_401 헤더 누락 → HTTPException 401
test_verify_with_wrong_key_raises_401 헤더 X-WebAI-Key: wrong → HTTPException 401

7.2 통합 (stock/app/test_webai_endpoints.py, 12 케이스)

FastAPI TestClient + WEBAI_API_KEY monkeypatch + 임시 sqlite seed.

portfolio:

  • test_portfolio_normal_response_includes_pnl_pct
  • test_portfolio_summary_has_total_pnl_pct
  • test_portfolio_pnl_pct_matches_profit_rate_divided_100
  • test_portfolio_missing_key_returns_401

news-sentiment:

  • test_news_sentiment_returns_latest_date_when_no_param
  • test_news_sentiment_filters_by_date_param
  • test_news_sentiment_empty_table_returns_count_zero
  • test_news_sentiment_items_sorted_by_score_desc

공통:

  • test_401_response_has_no_payload_leak
  • test_503_when_webai_key_not_configured
  • test_wrong_key_returns_401
  • test_news_sentiment_unknown_date_returns_empty

7.3 Manual smoke (배포 후)

# 정상 통과
curl -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio
# → 200, JSON 응답에 pnl_pct 필드 존재

# 인증 실패
curl -i https://gahusb.synology.me/api/webai/portfolio
# → 401 + {"detail": "invalid or missing X-WebAI-Key"}

# news-sentiment
curl -H "X-WebAI-Key: $WEBAI_API_KEY" "https://gahusb.synology.me/api/webai/news-sentiment?date=2026-05-15"
# → 200, items 배열

# rate limit
for i in {1..100}; do curl -s -o /dev/null -w "%{http_code}\n" \
  -H "X-WebAI-Key: $WEBAI_API_KEY" \
  https://gahusb.synology.me/api/webai/portfolio; done | sort | uniq -c
# → 200 다수 + 429 일부

8. 위험 및 완화

위험 완화
운영 .env 의 WEBAI_API_KEY 누락 → web-ai 호출 503 startup 시점 ERROR log + Phase 2 web-ai 구현 시 startup health check 로 즉시 발견
키 노출 (.env 유출) 회전 — NAS .env + web-ai .env 동시 갱신 + 컨테이너 재기동. 다운타임 ~10초
nginx rate limit 너무 빡빡해서 web-ai 정상 폴링 차단 60r/m + burst=20 은 web-ai 폴링 (분당 3 call) 대비 20배 여유. Phase 7 운영 모니터링에서 조정
pnl_pct 단위 실수 (백분율 vs 비율) 단위 명세 (비율, 0.047) 명시 + test_portfolio_pnl_pct_matches_profit_rate_divided_100 으로 검증
news_sentiment 테이블 empty 응답 {"date": null, "count": 0, "items": []} (테스트 케이스 포함)
/api/webai/portfolio vs /api/portfolio 응답 drift 둘 다 동일 get_portfolio() 내부 함수 호출 + webai 측 augment mapper 만 적용. drift 회피
nginx 가 underscore 헤더 drop X-WebAI-Key (dash) 사용으로 회피. 명시적 forward 도 추가
외부에서 endpoint 무인증 접근 시도 logger.warning 으로 IP 1회 기록 (대량 시도 시 IDS/alert 검토는 별도)
키 brute force 시도 nginx rate limit 분당 60 + 키 64자 랜덤 → 현실적 brute force 불가능

9. 운영 영향

항목 영향
다운타임 ~10초 (stock + nginx 재기동)
사용자 영향 없음 (web-ui 무변경)
운영 .env 갱신 1회 (WEBAI_API_KEY=<랜덤>)
frontend 재배포 불필요
다른 lab 영향 없음
DB 마이그레이션 없음 (news_sentiment 테이블 기존, 추가 컬럼 없음)

10. Phase 1 완료 조건 (DoD)

  • stock/app/auth.py 신규 + 단위 테스트 3 PASS
  • stock/app/main.py 의 2 신규 endpoint + 통합 테스트 12 PASS
  • nginx/default.conflimit_req_zone webai + /api/webai/ location 추가
  • docker-compose.yml 의 stock env WEBAI_API_KEY 추가
  • 운영 .env 갱신 (사용자 1회) — 본 Phase plan 의 마지막 task
  • 배포 후 manual smoke 4 항목 PASS (정상 200 / 인증 누락 401 / news-sentiment 200 / rate limit 429)
  • stock pytest 전체 86 + 신규 15 = 101 PASS
  • web-ui 영향 없음 검증 (web-ui 의 /api/portfolio 정상 동작)

11. Phase 2 와의 관계

본 Phase 1 완료 후 즉시 Phase 2 (web-ai pull worker + signal API client) spec → plan → 구현. 의존성:

[Phase 1 spec/plan/실행]   →   [Phase 2 spec/plan/실행]
       1주                              2주

Phase 2 의 입력 계약 = 본 spec 의 §4 API 계약. Phase 2 client 가 본 endpoint 들을 polling + 캐시 + retry.

Phase 2 시작 시점 검증 항목:

  • web-ai 의 .envWEBAI_API_KEY 설정
  • web-ai 의 httpx client 가 X-WebAI-Key 헤더 자동 첨부
  • 429 응답 시 backoff 정책 (exponential, max 60s)
  • 5xx 응답 시 short retry (3회) 후 alert

12. Backlog (본 spec NOT)

V2 운영 후 별도 슬라이스로:

  • /api/webai/screener/run 신규 endpoint — 현재 /api/stock/screener/run 직접 호출, drift 발견 시 분리
  • portfolio 의 entry_date / days_held / position_weight 추가 — sell signal 정밀화 시
  • ticker filter — news-sentiment 의 ?tickers= 옵션 (Top-20 만 가져올 때 payload 절약)
  • 사용자 인증 도입 (Lab 사이트 통합 로그인) — 기존 /api/portfolio 무인증 외부 노출 해결
  • nginx 응답 시간/에러율 메트릭 + 텔레그램 alert — Phase 7 모니터링 통합
  • HMAC 서명 옵션 — 외부 노출 endpoint 추가 시 검토
  • Key rotation 자동화 — 일정 운영 안정화 후