From 094366a162a591efdd4f2f8d52f014016f19ffd6 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 15 May 2026 08:24:37 +0900 Subject: [PATCH] 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) --- .../2026-05-15-signal-v2-phase1-webai-api.md | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md diff --git a/docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md b/docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md new file mode 100644 index 0000000..3521f80 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md @@ -0,0 +1,369 @@ +# 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: +``` + +응답 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: +``` + +쿼리: +- `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 자동화 — 일정 운영 안정화 후