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

370 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 추가:
```json
{
"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:
```json
{
"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`:
```python
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` 적용:
```python
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 옆에 추가)
```nginx
limit_req_zone $binary_remote_addr zone=webai:5m rate=60r/m;
```
### 6.2 `server {}` 블록 내 신규 location (`/api/stock/` location 위에 우선순위)
```nginx
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 (배포 후)
```bash
# 정상 통과
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.conf``limit_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 의 `.env``WEBAI_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 자동화 — 일정 운영 안정화 후