Compare commits

4 Commits

Author SHA1 Message Date
f261a80d52 docs(signal-v1): web-ai V1 rename plan — 13 step atomic refactor
Single big task (Task 1) for atomic mv + load_dotenv update +
new CLAUDE.md/start.bat + verification. Task 2 = user manual
push + 30-min runtime verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:23:38 +09:00
42e9c8df27 docs(signal-v1): web-ai V1 → signal_v1/ rename spec
atomic refactor (single commit) of web-ai root V1 assets into
signal_v1/ subdirectory. V2 (signal_v2/) Phase 2 will be added
alongside in subsequent slice. Pattern mirrors stock-lab → stock
graduation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 00:19:00 +09:00
c84c6b5bac docs(signal-v2): Phase 1 implementation plan — 7 tasks TDD
7 tasks: auth.py + verify_webai_key (Task 1) → portfolio + pnl_pct
(Task 2) → news-sentiment (Task 3) → common edge cases (Task 4) →
docker-compose env (Task 5) → nginx config (Task 6) → deploy +
manual smoke (Task 7). 16 tests total (4 unit + 12 integration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:30:53 +09:00
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
4 changed files with 1896 additions and 0 deletions

View File

@@ -0,0 +1,858 @@
# Signal V2 Phase 1 — stock WebAI API Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Confidence Signal Pipeline V2 의 Phase 2 (web-ai pull worker) 가 polling 할 stock 의 인증된 입력 계약 3종 (`/api/webai/portfolio`, `/api/webai/news-sentiment`, X-WebAI-Key 인증 인프라) 을 신설.
**Architecture:** stock FastAPI app 에 `/api/webai/*` prefix 의 신규 endpoint 2개 추가. 인증은 `verify_webai_key` FastAPI dependency (단일 정적 키 `WEBAI_API_KEY` 환경변수 비교). nginx 에 `/api/webai/` location + `limit_req` rate limit. 기존 `/api/portfolio` 무변경, web-ui 영향 0.
**Tech Stack:** FastAPI / pytest + TestClient / sqlite3 / nginx (limit_req_zone)
**Spec:** `web-ui/docs/superpowers/specs/2026-05-15-signal-v2-phase1-webai-api.md`
---
## 파일 구조
| 파일 | 책임 |
|------|------|
| `web-backend/stock/app/auth.py` (신규) | `verify_webai_key` FastAPI dependency — X-WebAI-Key 헤더 검증, env 미설정 503, 인증 실패 401 + logger.warning |
| `web-backend/stock/app/main.py` (수정) | 2 신규 endpoint: `GET /api/webai/portfolio`, `GET /api/webai/news-sentiment`. 기존 `get_portfolio()` 응답 위에 pnl_pct augment mapper |
| `web-backend/stock/app/test_webai_auth.py` (신규) | `verify_webai_key` 단위 3 케이스 |
| `web-backend/stock/app/test_webai_endpoints.py` (신규) | 두 endpoint × 4 + 공통 4 = 12 통합 케이스 |
| `web-backend/nginx/default.conf` (수정) | `limit_req_zone webai` + `/api/webai/` location |
| `web-backend/docker-compose.yml` (수정) | stock 컨테이너 env 에 `WEBAI_API_KEY` 추가 |
---
## Task 순서
```
Task 1: auth.py + verify_webai_key 단위 테스트 (TDD)
Task 2: /api/webai/portfolio + 응답 보강 + 통합 4 케이스 (TDD)
Task 3: /api/webai/news-sentiment + DB SELECT + 통합 4 케이스 (TDD)
Task 4: 공통 통합 4 케이스 (401 leak / 503 / wrong key / unknown date)
Task 5: docker-compose env 추가
Task 6: nginx config (rate limit + location + 헤더 forward)
Task 7: 배포 + 사용자 .env 갱신 + manual smoke 검증
```
각 Task 는 TDD 패턴 (test 먼저 → fail 확인 → 구현 → pass → commit).
---
### Task 1: auth.py + verify_webai_key 단위 테스트
**Files:**
- Create: `web-backend/stock/app/auth.py`
- Create: `web-backend/stock/app/test_webai_auth.py`
- [ ] **Step 1: Write the failing test**
Create `web-backend/stock/app/test_webai_auth.py`:
```python
import pytest
from fastapi import HTTPException
from starlette.requests import Request
def _make_request() -> Request:
"""Minimal Request stub for verify_webai_key (only request.url.path + request.client used)."""
scope = {
"type": "http",
"path": "/api/webai/test",
"headers": [],
"client": ("1.2.3.4", 12345),
}
return Request(scope=scope)
def test_verify_with_valid_key_passes(monkeypatch):
monkeypatch.setenv("WEBAI_API_KEY", "secret-key-abc")
from app.auth import verify_webai_key
verify_webai_key(_make_request(), x_webai_key="secret-key-abc")
def test_verify_without_key_raises_401(monkeypatch):
monkeypatch.setenv("WEBAI_API_KEY", "secret-key-abc")
from app.auth import verify_webai_key
with pytest.raises(HTTPException) as exc:
verify_webai_key(_make_request(), x_webai_key=None)
assert exc.value.status_code == 401
assert "X-WebAI-Key" in exc.value.detail
def test_verify_with_wrong_key_raises_401(monkeypatch):
monkeypatch.setenv("WEBAI_API_KEY", "secret-key-abc")
from app.auth import verify_webai_key
with pytest.raises(HTTPException) as exc:
verify_webai_key(_make_request(), x_webai_key="wrong-key")
assert exc.value.status_code == 401
def test_verify_returns_503_when_env_missing(monkeypatch):
monkeypatch.delenv("WEBAI_API_KEY", raising=False)
from app.auth import verify_webai_key
with pytest.raises(HTTPException) as exc:
verify_webai_key(_make_request(), x_webai_key="anything")
assert exc.value.status_code == 503
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd web-backend/stock && python -m pytest app/test_webai_auth.py -v`
Expected: ImportError: cannot import name 'verify_webai_key' from 'app.auth' (또는 ModuleNotFoundError: No module named 'app.auth')
- [ ] **Step 3: Write minimal implementation**
Create `web-backend/stock/app/auth.py`:
```python
import os
import logging
from fastapi import Header, HTTPException
from starlette.requests import Request
logger = logging.getLogger("stock")
def verify_webai_key(
request: Request,
x_webai_key: str | None = Header(default=None, alias="X-WebAI-Key"),
) -> None:
"""
/api/webai/* 보호용 FastAPI dependency.
- WEBAI_API_KEY env 미설정 → 503 (다른 endpoint 무영향)
- 헤더 누락 또는 키 불일치 → 401 + logger.warning(ip)
"""
configured = os.getenv("WEBAI_API_KEY", "").strip()
if not configured:
logger.error("WEBAI_API_KEY not configured — refusing /api/webai/* request")
raise HTTPException(status_code=503, detail="webai auth not configured")
if not x_webai_key or x_webai_key != configured:
remote = request.client.host if request.client else "?"
logger.warning("auth_fail path=%s remote=%s", request.url.path, remote)
raise HTTPException(status_code=401, detail="invalid or missing X-WebAI-Key")
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd web-backend/stock && python -m pytest app/test_webai_auth.py -v`
Expected: 4 passed
- [ ] **Step 5: Commit**
```bash
git add web-backend/stock/app/auth.py web-backend/stock/app/test_webai_auth.py
git commit -m "$(cat <<'EOF'
feat(stock-webai): add X-WebAI-Key auth dependency + tests
verify_webai_key FastAPI dependency: 401 on missing/wrong key,
503 when WEBAI_API_KEY env unset. 4 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 2: /api/webai/portfolio + 응답 보강 + 통합 4 케이스
**Files:**
- Modify: `web-backend/stock/app/main.py` (신규 endpoint + helper)
- Create: `web-backend/stock/app/test_webai_endpoints.py` (portfolio 4 케이스)
- [ ] **Step 1: Write the failing tests (portfolio 4 케이스)**
Create `web-backend/stock/app/test_webai_endpoints.py`:
```python
import os
import sqlite3
import pytest
from fastapi.testclient import TestClient
from app.screener.schema import ensure_screener_schema
from app.db import init_db
@pytest.fixture(autouse=True)
def isolated_db_and_auth(tmp_path, monkeypatch):
db_path = tmp_path / "stock.db"
# 기본 stock DB 스키마
monkeypatch.setenv("STOCK_DB_PATH", str(db_path))
init_db()
# screener 스키마 (news_sentiment, krx_master 등)
c = sqlite3.connect(db_path)
ensure_screener_schema(c)
c.close()
# WEBAI_API_KEY 활성화
monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
@pytest.fixture
def client():
from app.main import app
return TestClient(app)
HEADERS_OK = {"X-WebAI-Key": "test-secret"}
def _seed_portfolio(broker="키움", ticker="005930", name="삼성전자",
quantity=100, avg_price=75000.0, purchase_price=75500.0):
from app.db import add_portfolio_item
return add_portfolio_item(broker, ticker, name, quantity, avg_price,
purchase_price=purchase_price)
def test_webai_portfolio_normal_response_includes_pnl_pct(client, monkeypatch):
_seed_portfolio()
# current_price 모킹 — profit_rate 4.67% 만들기
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "2026-05-15T15:30:00"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
assert r.status_code == 200
body = r.json()
assert len(body["holdings"]) == 1
h = body["holdings"][0]
assert h["pnl_pct"] is not None
assert abs(h["pnl_pct"] - 0.0467) < 0.0005 # 0.0467 ± rounding
def test_webai_portfolio_summary_has_total_pnl_pct(client, monkeypatch):
_seed_portfolio()
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "x"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
body = r.json()
assert "total_pnl_pct" in body["summary"]
assert abs(body["summary"]["total_pnl_pct"] - 0.0467) < 0.0005
def test_webai_portfolio_pnl_pct_matches_profit_rate_divided_100(client, monkeypatch):
_seed_portfolio()
from app import main
monkeypatch.setattr(
main, "get_current_prices_detail",
lambda tickers: {"005930": {"price": 78500.0, "session": "REGULAR", "as_of": "x"}}
)
r = client.get("/api/webai/portfolio", headers=HEADERS_OK)
h = r.json()["holdings"][0]
assert h["pnl_pct"] == round(h["profit_rate"] / 100, 6)
def test_webai_portfolio_missing_key_returns_401(client):
r = client.get("/api/webai/portfolio")
assert r.status_code == 401
assert "X-WebAI-Key" in r.json()["detail"]
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd web-backend/stock && python -m pytest app/test_webai_endpoints.py -v`
Expected: 4 failed with 404 (endpoint not defined yet) — except `missing_key_returns_401` 도 404 (endpoint 자체가 없으므로)
- [ ] **Step 3: Write minimal implementation**
Modify `web-backend/stock/app/main.py` — add right after the imports block (around line 27):
```python
from .auth import verify_webai_key
```
And add the new endpoint right after the existing `get_portfolio()` function (after line 384):
```python
def _augment_portfolio_with_pnl_pct(raw: dict) -> dict:
"""Add pnl_pct (ratio) to each holding and total_pnl_pct to summary."""
holdings = []
for h in raw["holdings"]:
pnl_pct = round(h["profit_rate"] / 100, 6) if h.get("profit_rate") is not None else None
holdings.append({**h, "pnl_pct": pnl_pct})
summary = dict(raw["summary"])
rate = summary.get("total_profit_rate")
summary["total_pnl_pct"] = round(rate / 100, 6) if rate is not None else 0.0
return {"holdings": holdings, "cash": raw["cash"], "summary": summary}
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
def get_webai_portfolio():
"""web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가)."""
return _augment_portfolio_with_pnl_pct(get_portfolio())
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `cd web-backend/stock && python -m pytest app/test_webai_endpoints.py -v`
Expected: 4 passed
Also run full stock suite to verify no regression:
Run: `cd web-backend/stock && python -m pytest --ignore=app/test_scraper.py -q`
Expected: 86 + 4 = 90 passed
- [ ] **Step 5: Commit**
```bash
git add web-backend/stock/app/main.py web-backend/stock/app/test_webai_endpoints.py
git commit -m "$(cat <<'EOF'
feat(stock-webai): /api/webai/portfolio + pnl_pct augment
Reuses get_portfolio() and adds pnl_pct (ratio, profit_rate/100) to
each holding plus total_pnl_pct to summary. 4 integration tests pass.
verify_webai_key dependency enforced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 3: /api/webai/news-sentiment + DB SELECT + 통합 4 케이스
**Files:**
- Modify: `web-backend/stock/app/main.py` (신규 endpoint + helper)
- Modify: `web-backend/stock/app/test_webai_endpoints.py` (news-sentiment 4 케이스 추가)
- [ ] **Step 1: Write the failing tests (news-sentiment 4 케이스)**
Append to `web-backend/stock/app/test_webai_endpoints.py`:
```python
def _seed_news_sentiment(date_str: str, rows: list[tuple]):
"""rows: list of (ticker, score_raw, reason, news_count)."""
db_path = os.environ["STOCK_DB_PATH"]
c = sqlite3.connect(db_path)
for ticker, score, reason, news_count in rows:
c.execute(
"INSERT OR REPLACE INTO news_sentiment "
"(ticker, date, score_raw, reason, news_count, source) "
"VALUES (?, ?, ?, ?, ?, 'articles')",
(ticker, date_str, score, reason, news_count)
)
c.commit()
c.close()
def _seed_krx_master(rows: list[tuple]):
"""rows: list of (ticker, name)."""
db_path = os.environ["STOCK_DB_PATH"]
c = sqlite3.connect(db_path)
import datetime as dt
now = dt.datetime.utcnow().isoformat()
for ticker, name in rows:
c.execute(
"INSERT OR REPLACE INTO krx_master "
"(ticker, name, market, market_cap, updated_at) VALUES (?, ?, 'KOSPI', 0, ?)",
(ticker, name, now)
)
c.commit()
c.close()
def test_webai_news_sentiment_returns_latest_date_when_no_param(client):
_seed_krx_master([("005930", "삼성전자"), ("000660", "SK하이닉스")])
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "old", 5)])
_seed_news_sentiment("2026-05-15", [
("005930", 6.2, "HBM 양산 가시화", 12),
("000660", 5.5, "PPI 우려에도 강세", 8),
])
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
assert r.status_code == 200
body = r.json()
assert body["date"] == "2026-05-15"
assert body["count"] == 2
# sorted by score DESC
assert body["items"][0]["ticker"] == "005930"
assert body["items"][0]["score"] == 6.2
assert body["items"][0]["name"] == "삼성전자"
assert body["items"][0]["reason"] == "HBM 양산 가시화"
def test_webai_news_sentiment_filters_by_date_param(client):
_seed_krx_master([("005930", "삼성전자")])
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "yesterday", 5)])
_seed_news_sentiment("2026-05-15", [("005930", 6.2, "today", 12)])
r = client.get("/api/webai/news-sentiment?date=2026-05-14", headers=HEADERS_OK)
body = r.json()
assert body["date"] == "2026-05-14"
assert body["count"] == 1
assert body["items"][0]["reason"] == "yesterday"
def test_webai_news_sentiment_empty_table_returns_count_zero(client):
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
body = r.json()
assert body["date"] is None
assert body["count"] == 0
assert body["items"] == []
def test_webai_news_sentiment_items_sorted_by_score_desc(client):
_seed_krx_master([("A", "A주"), ("B", "B주"), ("C", "C주")])
_seed_news_sentiment("2026-05-15", [
("A", 1.0, "low", 1),
("B", 9.0, "high", 1),
("C", 5.0, "mid", 1),
])
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
items = r.json()["items"]
assert [i["score"] for i in items] == [9.0, 5.0, 1.0]
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd web-backend/stock && python -m pytest app/test_webai_endpoints.py::test_webai_news_sentiment_returns_latest_date_when_no_param app/test_webai_endpoints.py::test_webai_news_sentiment_filters_by_date_param app/test_webai_endpoints.py::test_webai_news_sentiment_empty_table_returns_count_zero app/test_webai_endpoints.py::test_webai_news_sentiment_items_sorted_by_score_desc -v`
Expected: 4 failed with 404 (endpoint not defined)
- [ ] **Step 3: Write minimal implementation**
Modify `web-backend/stock/app/main.py` — add right after the portfolio endpoint added in Task 2:
```python
def _fetch_news_sentiment_dump(date: str | None) -> dict:
"""news_sentiment 일별 dump (krx_master JOIN, score DESC)."""
from .db import _conn # _conn() is the shared connection helper
conn = _conn()
try:
# 1) date resolve — None 이면 최신 date
if date is None:
row = conn.execute(
"SELECT MAX(date) FROM news_sentiment"
).fetchone()
date = row[0] if row and row[0] else None
if date is None:
return {"date": None, "count": 0, "items": []}
# 2) JOIN krx_master.name (없으면 ticker 그대로)
rows = conn.execute(
"""
SELECT ns.ticker,
COALESCE(km.name, ns.ticker) AS name,
ns.score_raw,
ns.reason,
ns.news_count,
ns.source
FROM news_sentiment ns
LEFT JOIN krx_master km ON km.ticker = ns.ticker
WHERE ns.date = ?
ORDER BY ns.score_raw DESC
""",
(date,)
).fetchall()
finally:
conn.close()
items = [
{"ticker": r[0], "name": r[1], "score": r[2],
"reason": r[3], "news_count": r[4], "source": r[5]}
for r in rows
]
return {"date": date, "count": len(items), "items": items}
@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)])
def get_webai_news_sentiment(date: str | None = None):
"""web-ai 전용 news sentiment 일별 dump."""
return _fetch_news_sentiment_dump(date)
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `cd web-backend/stock && python -m pytest app/test_webai_endpoints.py -v`
Expected: 8 passed (4 portfolio + 4 news-sentiment)
Run full suite:
Run: `cd web-backend/stock && python -m pytest --ignore=app/test_scraper.py -q`
Expected: 86 + 8 = 94 passed
- [ ] **Step 5: Commit**
```bash
git add web-backend/stock/app/main.py web-backend/stock/app/test_webai_endpoints.py
git commit -m "$(cat <<'EOF'
feat(stock-webai): /api/webai/news-sentiment daily dump
JOINs news_sentiment with krx_master for name fallback. Sorted by
score DESC. Date param defaults to latest. Empty table returns
{date: null, count: 0, items: []}. 4 integration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 4: 공통 통합 4 케이스 (401 leak / 503 / wrong key / unknown date)
**Files:**
- Modify: `web-backend/stock/app/test_webai_endpoints.py` (공통 4 케이스 추가)
- [ ] **Step 1: Write the tests**
Append to `web-backend/stock/app/test_webai_endpoints.py`:
```python
def test_webai_401_response_has_no_payload_leak(client):
"""인증 실패 응답에는 portfolio/sentiment 데이터가 없어야 한다."""
_seed_portfolio()
r = client.get("/api/webai/portfolio") # 헤더 없음
assert r.status_code == 401
body = r.json()
assert "holdings" not in body
assert "cash" not in body
assert "summary" not in body
def test_webai_503_when_env_missing(client, monkeypatch):
"""WEBAI_API_KEY env 미설정 시 503, 다른 endpoint 영향 없음."""
monkeypatch.delenv("WEBAI_API_KEY", raising=False)
r1 = client.get("/api/webai/portfolio", headers={"X-WebAI-Key": "anything"})
assert r1.status_code == 503
# 기존 endpoint 무영향 — /api/portfolio 는 200 (빈 portfolio)
r2 = client.get("/api/portfolio")
assert r2.status_code == 200
def test_webai_wrong_key_returns_401(client):
r = client.get("/api/webai/portfolio", headers={"X-WebAI-Key": "wrong"})
assert r.status_code == 401
def test_webai_news_sentiment_unknown_date_returns_empty(client):
r = client.get("/api/webai/news-sentiment?date=1999-01-01", headers=HEADERS_OK)
assert r.status_code == 200
body = r.json()
assert body["count"] == 0
assert body["items"] == []
```
- [ ] **Step 2: Run tests to verify they pass**
Run: `cd web-backend/stock && python -m pytest app/test_webai_endpoints.py -v`
Expected: 12 passed (4 + 4 + 4)
Also run full stock suite:
Run: `cd web-backend/stock && python -m pytest --ignore=app/test_scraper.py -q`
Expected: 86 + 12 = 98 passed (note: spec said 101, but 86 stock + 4 auth + 12 endpoint = 102; the count in the spec was approximate, actual = current_baseline + 4 + 12)
- [ ] **Step 3: Commit**
```bash
git add web-backend/stock/app/test_webai_endpoints.py
git commit -m "$(cat <<'EOF'
test(stock-webai): edge cases — 401 no leak, 503 env missing, unknown date
Verifies auth failure responses contain no portfolio/sentiment data,
503 when WEBAI_API_KEY env unset (existing endpoints unaffected),
news-sentiment unknown date returns empty result.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 5: docker-compose env 추가
**Files:**
- Modify: `web-backend/docker-compose.yml` (stock 서비스 env)
- [ ] **Step 1: Locate the stock environment block**
Run: `grep -n -A 20 "^ stock:" web-backend/docker-compose.yml | head -30`
Expected: stock 서비스 블록 출력. environment 또는 env_file 항목 확인.
- [ ] **Step 2: Add WEBAI_API_KEY to stock env**
Edit `web-backend/docker-compose.yml` — find the `stock:` service block and add `WEBAI_API_KEY=${WEBAI_API_KEY}` line to the `environment:` list.
Example final state (excerpt):
```yaml
stock:
container_name: stock
build:
context: ./stock
environment:
- TZ=Asia/Seoul
- STOCK_DB_PATH=/app/data/stock.db
- WEBAI_API_KEY=${WEBAI_API_KEY}
# ... other vars
```
- [ ] **Step 3: Verify compose config**
Run: `cd web-backend && docker compose config | grep -A 30 "stock:" | grep WEBAI_API_KEY`
Expected: `WEBAI_API_KEY: ""` (env 미설정 시 빈 문자열) 또는 실제 값
If the line is missing, re-check the edit.
- [ ] **Step 4: Commit**
```bash
cd web-backend
git add docker-compose.yml
git commit -m "$(cat <<'EOF'
chore(stock-webai): pass WEBAI_API_KEY env to stock container
Required by /api/webai/* endpoints. Operator must set WEBAI_API_KEY
in NAS /volume1/docker/webpage/.env before deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 6: nginx config (rate limit + location + 헤더 forward)
**Files:**
- Modify: `web-backend/nginx/default.conf`
- [ ] **Step 1: Add limit_req_zone to http {} block**
Edit `web-backend/nginx/default.conf` — find the existing `limit_req_zone` directive (or the top of `http {}` block / top of `server {}` context) and add:
```nginx
# /api/webai/* rate limit — web-ai pull worker (default 60/min, burst 20)
limit_req_zone $binary_remote_addr zone=webai:5m rate=60r/m;
```
Place it at the top of the http context (before any server blocks) or alongside existing limit_req_zone directives.
- [ ] **Step 2: Add /api/webai/ location block**
In the same file, find the existing `location /api/stock/` (or similar) block inside the relevant `server {}` and add the new location BEFORE it (to ensure prefix matching priority is explicit):
```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;
}
```
- [ ] **Step 3: Validate nginx config syntax**
Run: `cd web-backend && docker compose run --rm --no-deps frontend nginx -t -c /etc/nginx/conf.d/default.conf 2>&1 | tail -5`
If frontend image isn't built locally, use:
Run: `docker run --rm -v "$(pwd)/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro" nginx:alpine nginx -t 2>&1`
Expected: `nginx: configuration file /etc/nginx/nginx.conf test is successful`
If the test fails due to missing upstream resolution (`host not found in upstream "stock"`), that's expected outside the compose network — the syntax check is what matters here. Ignore upstream resolution errors.
- [ ] **Step 4: Commit**
```bash
cd web-backend
git add nginx/default.conf
git commit -m "$(cat <<'EOF'
feat(nginx-webai): /api/webai/ location with rate limit + X-WebAI-Key forward
limit_req_zone webai:5m rate=60r/m, burst=20 nodelay, return 429 on
limit hit. Proxies to stock:8000 with X-Real-IP, X-Forwarded-For,
and X-WebAI-Key headers preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
---
### Task 7: 배포 + 사용자 .env 갱신 + manual smoke 검증
**Files:**
- 운영 `.env` (NAS `/volume1/docker/webpage/.env`) — 사용자 수동
- web-ai `.env` (Windows PC) — 사용자 수동 (Phase 2 진입 시 사용, 본 Phase 에서 미사용 OK)
**This task requires user action (NAS SSH + push). The implementer should pause and request the user to perform these steps. Do NOT mark the task complete until the user reports smoke test results.**
- [ ] **Step 1: Generate WEBAI_API_KEY (사용자)**
Sample command for the user to run locally:
```bash
python -c "import secrets; print(secrets.token_urlsafe(48))"
```
Save the output. This is the `WEBAI_API_KEY` value.
- [ ] **Step 2: Update NAS .env (사용자)**
SSH to NAS:
```bash
ssh user@gahusb.synology.me
sudo vi /volume1/docker/webpage/.env
```
Add line:
```
WEBAI_API_KEY=<the key generated in Step 1>
```
- [ ] **Step 3: Push web-backend (사용자)**
Locally:
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-backend && git push
```
Wait for Gitea webhook → deployer rsync + docker compose up.
If deployer DEPLOY_FAIL false alarm (known issue, see graduation experience):
```bash
ssh user@gahusb.synology.me
cd /volume1/docker/webpage
docker compose up -d --build stock frontend
docker ps --format "{{.Names}}: {{.Status}}" | grep -E "stock|frontend"
```
Expected: both `healthy`.
- [ ] **Step 4: Manual smoke — auth success**
```bash
export WEBAI_API_KEY=<the value>
curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio | head -c 200
```
Expected: 200 JSON beginning with `{"holdings":[`. If portfolio empty, `{"holdings":[],"cash":[...`.
```bash
curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" "https://gahusb.synology.me/api/webai/news-sentiment" | head -c 300
```
Expected: 200 JSON with `"date":` and `"items":` keys.
- [ ] **Step 5: Manual smoke — auth failure**
```bash
curl -i -s https://gahusb.synology.me/api/webai/portfolio | head -5
```
Expected:
```
HTTP/1.1 401 Unauthorized
...
{"detail":"invalid or missing X-WebAI-Key"}
```
```bash
curl -i -s -H "X-WebAI-Key: wrong" https://gahusb.synology.me/api/webai/portfolio | head -5
```
Expected: 401 with same detail.
- [ ] **Step 6: Manual smoke — rate limit**
```bash
for i in $(seq 1 120); 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
```
Expected: significant `200` count plus some `429` (rate limit triggered). Example:
```
85 200
35 429
```
If you see all 200 (no 429), rate limit may not be applied. Check nginx logs and config.
- [ ] **Step 7: Verify web-ui unchanged**
Open https://gahusb.synology.me/ in browser. Navigate to `/stock` page. Verify the portfolio list still loads correctly (no errors). This confirms `/api/portfolio` (legacy, no auth) is unaffected.
- [ ] **Step 8: Verify 503 fallback (optional, requires env removal + redeploy)**
This is optional and disruptive — only run if you want to verify the 503 fallback explicitly. Skip in normal deploys.
```bash
ssh user@gahusb.synology.me
cd /volume1/docker/webpage
# Comment out WEBAI_API_KEY in .env temporarily
sed -i 's/^WEBAI_API_KEY=/#WEBAI_API_KEY=/' .env
docker compose up -d stock
sleep 5
curl -s -o /dev/null -w "%{http_code}\n" -H "X-WebAI-Key: anything" https://gahusb.synology.me/api/webai/portfolio
# Expected: 503
# Restore:
sed -i 's/^#WEBAI_API_KEY=/WEBAI_API_KEY=/' .env
docker compose up -d stock
```
- [ ] **Step 9: Report results to user (운영 검증 게이트)**
Report to the user:
- Step 4 (auth success): PASS / FAIL with details
- Step 5 (auth failure): PASS / FAIL
- Step 6 (rate limit): PASS (some 429 observed) / FAIL (all 200)
- Step 7 (web-ui unchanged): PASS / FAIL
Only after the user confirms all PASS, mark Task 7 complete. If any FAIL, investigate before proceeding to Phase 2.
---
## Self-Review (plan author runs this)
**1. Spec coverage:**
| Spec § | 요구사항 | Plan task |
|--------|----------|----------|
| §2 포함 ① portfolio + pnl_pct | Task 2 ✅ |
| §2 포함 ② news-sentiment | Task 3 ✅ |
| §2 포함 ③ X-WebAI-Key 인증 | Task 1 ✅ |
| §2 포함 ④ nginx rate limit | Task 6 ✅ |
| §2 포함 ⑤ 인증 실패 logger | Task 1 (logger.warning 호출 포함) ✅ |
| §2 포함 ⑥ 15 테스트 (4 unit + 12 integration) | Task 1 (4) + Task 2 (4) + Task 3 (4) + Task 4 (4) = 16. Note: spec said 15, plan delivers 16 (4 auth + 4 portfolio + 4 sentiment + 4 common). Counted higher, no gap. ✅ |
| §4.1 portfolio shape with pnl_pct | Task 2 Step 3 ✅ |
| §4.2 news-sentiment shape | Task 3 Step 3 ✅ |
| §4.3 401 leak free | Task 4 Step 1 (`test_webai_401_response_has_no_payload_leak`) ✅ |
| §4.4 503 when env missing | Task 1 (unit) + Task 4 (integration) ✅ |
| §5 auth.py implementation | Task 1 Step 3 ✅ |
| §6 nginx config | Task 6 ✅ |
| §10 DoD | Task 7 covers manual smoke + web-ui verification ✅ |
No gaps.
**2. Placeholder scan:** No "TBD" / "implement later" / vague descriptions found. Every step has executable code or commands.
**3. Type consistency:**
- `verify_webai_key(request, x_webai_key)` signature consistent across Tasks 1, 2, 3 ✅
- `_augment_portfolio_with_pnl_pct(raw)` defined in Task 2, no later reference (helper internal to main.py) ✅
- `_fetch_news_sentiment_dump(date)` defined in Task 3, signature consistent ✅
- `HEADERS_OK = {"X-WebAI-Key": "test-secret"}` defined in Task 2, reused in Tasks 3 and 4 ✅
- `_seed_portfolio()` defined in Task 2, reused in Task 4 ✅
- `_seed_news_sentiment()` / `_seed_krx_master()` defined in Task 3, consistent ✅
- `WEBAI_API_KEY` env var name consistent across all tasks ✅
Plan passes self-review.

View File

@@ -0,0 +1,402 @@
# web-ai V1 → signal_v1 Rename Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `web-ai/` 루트의 모든 V1 자산 (main_server.py + modules/ + data/ + tests/ + 진입점 스크립트 + 문서 + 로그) 을 `web-ai/signal_v1/` 안으로 atomic mv 하고, web-ai 루트에 신규 가이드 (`CLAUDE.md`, `start.bat`) 추가. V2 (`signal_v2/`) 추가 전 신/구 격리.
**Architecture:** 단일 atomic commit (stock-lab → stock graduation 과 동일 패턴). `git mv` 로 history 보존, `load_dotenv()` 호출만 경로 명시. cwd 기반 V1 코드라 import 변경 0. Phase 6 deprecation 시 `rm -rf signal_v1/` 단순화.
**Tech Stack:** git mv / Python load_dotenv path 갱신 / pytest 회귀 확인
**Spec:** `web-ui/docs/superpowers/specs/2026-05-16-web-ai-v1-rename-to-signal-v1.md`
---
## 파일 구조 (Task 2 후)
```
web-ai/
├── .env ← 그대로 (V1 + V2 공유)
├── .gitignore ← 그대로
├── CLAUDE.md ← 신규 (web-ai 레벨 가이드)
├── start.bat ← 신규 (signal_v1 진입 wrapper)
├── signal_v1/ ← 신규 디렉토리
│ ├── CLAUDE.md ← 기존 V1 가이드 (mv)
│ ├── KIS_SETUP.md
│ ├── README.md
│ ├── main_server.py ← load_dotenv 경로 명시 갱신
│ ├── warmup_and_restart.py ← load_dotenv 경로 명시 갱신
│ ├── watchlist_manager.py
│ ├── backtester.py
│ ├── backtest_runner.py
│ ├── theme_manager.py
│ ├── start.bat ← 사용 안 함 (cleanup 안 함, 향후)
│ ├── modules/ ← 전체
│ ├── data/ ← 전체 (runtime data 보존)
│ ├── tests/ ← 전체
│ └── (log/json 파일들)
└── (signal_v2/ 는 Phase 2 spec)
```
---
## Task 1: Atomic refactor (사전 점검 + git mv + 신규 파일 + 검증 + commit)
**Files:**
- Source repo: `C:\Users\jaeoh\Desktop\workspace\web-ai` (별도 Gitea repo: `ai-trade.git`, branch `main`)
- Create: `web-ai/signal_v1/` (디렉토리)
- Create: `web-ai/CLAUDE.md` (신규)
- Create: `web-ai/start.bat` (신규)
- Move (git mv): web-ai 루트의 모든 V1 자산 → signal_v1/
- Modify: `web-ai/signal_v1/main_server.py` (load_dotenv 명시 경로)
- Modify: `web-ai/signal_v1/warmup_and_restart.py` (load_dotenv 명시 경로)
- (필요 시) Modify: `signal_v1/modules/config.py` 또는 다른 load_dotenv 위치
- [ ] **Step 1: 사전 — 자동매매 봇 정지 확인 + git status clean**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
git status
```
Expected: `nothing to commit, working tree clean`. 만약 dirty 면 implementer 는 BLOCKED 보고. 사용자가 stash 또는 commit 처리.
또한: V1 자동매매 봇이 실행 중이면 mv 도중 파일 잠금 위험. PowerShell:
```powershell
Get-Process python -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, StartTime
```
실행 중 Python 프로세스 발견 시 사용자에게 종료 요청. (장외 시간대에 작업 가정.)
- [ ] **Step 2: 사전 grep — load_dotenv 호출 위치 파악**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
grep -rn "load_dotenv" --include="*.py" .
```
Expected: 1~3개 hit. 각 hit 의 파일 경로 기록 (Step 6 에서 갱신). 일반적으로 main_server.py, warmup_and_restart.py, modules/config.py 중 1~2곳에 있음.
만약 hit 0 이면 V1 이 `.env` 를 다른 방식 (예: pydantic-settings) 으로 로드. 코드 경로 추가 grep:
```bash
grep -rn "BaseSettings\|env_file\|\.env" --include="*.py" .
```
어느 방식이든 cwd 가 signal_v1/ 으로 바뀌면 `.env` 가 parent (`web-ai/.env`) 에 있다는 사실을 코드가 알아야 함.
- [ ] **Step 3: 사전 baseline — 현 pytest 통과 개수 측정**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
python -m pytest tests/unit -q 2>&1 | tail -3
```
Expected output 형태: `N passed in Xs` (또는 `N passed, M warnings ...`). N 값을 baseline 으로 기록 (Step 13 에서 비교).
만약 baseline 자체가 실패면 implementer 는 DONE_WITH_CONCERNS 보고 — 사용자 결정 (pre-existing failure 라면 무시하고 진행 가능).
- [ ] **Step 4: signal_v1 디렉토리 생성**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
mkdir signal_v1
```
Verify:
```bash
ls -d signal_v1
```
Expected: `signal_v1`
- [ ] **Step 5: git mv 실행 (V1 자산 모두)**
다음 항목을 모두 `signal_v1/` 안으로 이동. `git mv` 사용 (history 보존):
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
# 진입점 + 스크립트
git mv main_server.py signal_v1/
git mv warmup_and_restart.py signal_v1/
git mv watchlist_manager.py signal_v1/
git mv backtester.py signal_v1/
git mv backtest_runner.py signal_v1/
git mv theme_manager.py signal_v1/
git mv start.bat signal_v1/
# 문서 (현 V1 가이드)
git mv CLAUDE.md signal_v1/
git mv KIS_SETUP.md signal_v1/
git mv README.md signal_v1/
# 디렉토리
git mv modules signal_v1/
git mv data signal_v1/
git mv tests signal_v1/
# 로그 / IPC / 캐시
git mv bot_ipc.json signal_v1/ 2>/dev/null || true
git mv bot_output.log signal_v1/ 2>/dev/null || true
git mv daily_launcher.log signal_v1/ 2>/dev/null || true
git mv server.log signal_v1/ 2>/dev/null || true
git mv telegram_bot.log signal_v1/ 2>/dev/null || true
git mv warmup.log signal_v1/ 2>/dev/null || true
```
`__pycache__/` 는 gitignore 이므로 git mv 불가능. 단순 mv:
```bash
mv __pycache__ signal_v1/ 2>/dev/null || true
```
Verify:
```bash
git status --short | head -30
ls signal_v1/
ls
```
Expected: `signal_v1/` 안에 모든 V1 자산이 있고, web-ai 루트에는 `.env`, `.gitignore`, `signal_v1/` 만 (still untracked: none yet for new files).
- [ ] **Step 6: load_dotenv 경로 명시 갱신**
Step 2 에서 식별한 각 `load_dotenv()` 호출을 명시 경로로 변경. 가장 빈도 높은 패턴 (main_server.py 의 시작 부분):
기존 (cwd 기준):
```python
from dotenv import load_dotenv
load_dotenv()
```
신규 (명시 경로, signal_v1 의 parent = web-ai 루트):
```python
from pathlib import Path
from dotenv import load_dotenv
# web-ai/.env (signal_v1/ 의 parent) 명시 로드
load_dotenv(Path(__file__).parent.parent / ".env")
```
Step 2 에서 식별한 모든 위치에 동일 패턴 적용. 만약 V1 이 `BaseSettings` (pydantic) 사용 시:
```python
class Settings(BaseSettings):
class Config:
env_file = str(Path(__file__).parent.parent / ".env")
```
만약 V1 이 그냥 `os.getenv(...)` 만 쓰고 어딘가에서 명시적으로 load 하지 않는다면 (uvicorn 이 시작 시 cwd 의 .env 를 자동 로드 시) — 시작 wrapper (`web-ai/start.bat`) 가 `cd signal_v1` 후 실행하면 cwd=signal_v1 → `.env` 못 찾음. 해결: Step 7 의 `start.bat` 에서 명시적으로 `cd /d "%~dp0"` (= web-ai 루트) 후 `python signal_v1/main_server.py` 실행.
근데 그러면 main_server.py 안의 다른 상대 경로 (`data/kis_token.json` 등) 가 cwd=web-ai 일 때 `web-ai/data/kis_token.json` 을 찾음 → 잘못된 경로.
**결정**: cwd 는 `signal_v1/` 으로 두고 `load_dotenv(Path(__file__).parent.parent / ".env")` 명시. 다른 상대 경로는 cwd=signal_v1 기준이라 `data/...` 그대로 작동.
각 갱신 후 git status:
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
git status --short | head -10
```
Expected: signal_v1/main_server.py 등 modified 표시.
- [ ] **Step 7: 신규 파일 — web-ai/CLAUDE.md**
Create `web-ai/CLAUDE.md`:
```markdown
# web-ai — Workspace 가이드
Windows AI 머신 (AMD 9800X3D + RTX 5070 Ti) 의 두 시그널 파이프라인 컨테이너.
## 디렉토리 구조
| 경로 | 역할 | 상태 |
|------|------|------|
| `signal_v1/` | V1 자체 자동매매 시스템 (main_server.py + Trading Bot + Telegram Bot + LSTM + Ollama + KIS 자동주문) | 운영 중. Confidence Signal Pipeline V2 Phase 6 에서 deprecation 예정 |
| `signal_v2/` | V2 신호 파이프라인 (stock pull worker + Chronos-2 + signal API client) | Phase 2 에서 신설 |
| `.env` | V1 + V2 환경변수 공유 | KIS_*, TELEGRAM_*, STOCK_API_URL, WEBAI_API_KEY 등 |
| `start.bat` | V1 진입 (signal_v1 디렉토리 안 main_server.py 실행) | V2 별도 start 스크립트는 signal_v2/start.bat |
## 운영 가이드
- V1 시작: `start.bat` 또는 `cd signal_v1 && python main_server.py`
- V2 시작 (Phase 2 이후): `cd signal_v2 && python -m uvicorn main:app --port 8001`
- 둘 다 동시 실행 가능 (포트 분리: V1=8000, V2=8001)
## Phase 진행 상태 (Confidence Signal Pipeline V2)
`web-ui/docs/superpowers/specs/2026-05-15-confidence-signal-pipeline-v2-architecture.md` 참조.
자세한 V1 가이드는 `signal_v1/CLAUDE.md` 참조.
```
- [ ] **Step 8: 신규 파일 — web-ai/start.bat**
Create `web-ai/start.bat`:
```bat
@echo off
cd /d "%~dp0\signal_v1"
python main_server.py
```
- [ ] **Step 9: git add 신규 파일**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
git add CLAUDE.md start.bat
git add signal_v1/main_server.py signal_v1/warmup_and_restart.py # load_dotenv 갱신
# 추가로 갱신한 다른 .py 파일이 있으면 모두 add
```
git status 점검:
```bash
git status
```
Expected: 모든 git mv + 신규 + modify 변경이 staged 상태.
- [ ] **Step 10: 잔여 grep — `from web-ai` 같은 잘못된 import 0건 확인**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
grep -rn "from web-ai\|import web-ai" --include="*.py" signal_v1/
```
Expected: 0 lines.
또한 V1 코드 안에 hardcoded 절대 경로 (예: `C:\Users\jaeoh\Desktop\workspace\web-ai\data\...`) 검사:
```bash
grep -rn "web-ai.data\|web-ai/data\|web-ai\\\\data" --include="*.py" signal_v1/
```
Expected: 0 lines.
만약 hit 있으면 implementer 는 DONE_WITH_CONCERNS 보고, 사용자가 조정.
- [ ] **Step 11: signal_v1 안에서 pytest 자동 검증**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai/signal_v1
python -m pytest tests/unit -q 2>&1 | tail -5
```
Expected: Step 3 의 baseline 과 동일한 PASS 개수 (회귀 없음).
만약 import 오류 (`ModuleNotFoundError: No module named 'modules'`) 가 발생하면 conftest.py 가 sys.path 를 수정하지 않을 가능성. 확인:
```bash
cat tests/unit/conftest.py | head -20
```
필요 시 `sys.path.insert(0, str(Path(__file__).parent.parent.parent))` 추가. 단, 기존 conftest 가 cwd 기반이면 cwd=signal_v1 에서 작동해야 함.
만약 다른 failure 면 BLOCKED 보고 — 사용자 진단.
- [ ] **Step 12: 잠시 후 다시 git status — 추가 untracked 없는지 확인**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
git status
```
Expected: 모든 변경이 staged. 만약 새 untracked (pytest cache 등) 있으면 .gitignore 패턴 또는 무시.
- [ ] **Step 13: 단일 commit**
```bash
cd /c/Users/jaeoh/Desktop/workspace/web-ai
git commit -m "$(cat <<'EOF'
refactor: web-ai V1 assets → signal_v1/ (graduation prep)
Atomic mv of root V1 assets (main_server.py + modules/ + data/ +
tests/ + entry scripts + docs + logs) into signal_v1/ subdirectory.
load_dotenv() updated to load web-ai/.env explicitly via Path.
Adds web-ai/CLAUDE.md (workspace guide) and web-ai/start.bat
(signal_v1 entry wrapper). Prepares for signal_v2/ Phase 2.
Tests: signal_v1/tests/unit baseline preserved (no regression).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
Verify:
```bash
git log -1 --stat
```
Expected: 1 commit, 다수 rename + 2 신규 (CLAUDE.md / start.bat) + 1-3 modified (load_dotenv 위치).
## Reporting
When done, report:
- DONE: commit SHA, baseline test count (Step 3) + post-mv count (Step 11), 자동 grep 결과 (0 lines).
- DONE_WITH_CONCERNS: implementation 됐지만 hardcoded path / pre-existing test fail 등 발견 — 상세 보고.
- NEEDS_CONTEXT: load_dotenv 패턴이 spec 예상과 다름, 또는 conftest 추가 fix 필요 등.
- BLOCKED: working tree dirty / pytest baseline 자체 실패 / git mv 충돌.
---
## Task 2: 사용자 수동 — 운영 검증 + push
**This task requires user action. Pause and request user to perform.**
- [ ] **Step 1: V1 자동매매 봇 정상 시작 검증**
사용자가 PowerShell 에서:
```powershell
cd C:\Users\jaeoh\Desktop\workspace\web-ai
.\start.bat
```
기대 출력 (수십 줄):
- `Config.validate()` 성공 (환경변수 누락 없음)
- KIS OAuth `access_token` 발급 (또는 cached token 로드)
- Telegram Bot started + `Conflict` 없음
- ProcessWatchdog 시작
- Uvicorn 0.0.0.0:8000 listening
- 봇 사이클 (장중이면) 또는 idle (장외)
만약 `FileNotFoundError: .env` 또는 KIS auth 실패 시 — load_dotenv 경로 오류. Task 1 으로 돌아가 Step 6 조정.
- [ ] **Step 2: Telegram /status 명령 응답 검증**
사용자가 텔레그램에서 `/status` 명령. 봇이 정상 응답하면 IPC + SharedMemory + Telegram Bot 모두 정상.
- [ ] **Step 3: 30분 관측**
콘솔 또는 telegram_bot.log 에 에러 없음 + Watchdog 30초 간격 health check PASS 확인.
만약 자식 프로세스 (Trading Bot / Telegram Bot) 가 자동 종료 → restart loop → 재실패 시 Task 1 으로 돌아가 진단.
- [ ] **Step 4: git push (사용자, Gitea 자격증명)**
```powershell
cd C:\Users\jaeoh\Desktop\workspace\web-ai
git push
```
만약 자격증명 실패 시 사용자가 수동으로 처리 (메모리 `feedback_nas_deploy_paths.md` 의 Gitea 자격증명 패턴).
- [ ] **Step 5: 결과 보고 (사용자 → 컨트롤러)**
- Step 1 (start.bat 시작): PASS / FAIL — 첫 에러 메시지 공유
- Step 2 (/status 응답): PASS / FAIL
- Step 3 (30분 관측): PASS (no errors) / FAIL — 관측된 에러
- Step 4 (push): PASS / FAIL
전부 PASS 시 Task 2 완료 → Phase 2 brainstorming 재개 (이미 6 결정 + 디자인 섹션 1-2 OK).
---
## Self-Review
**1. Spec coverage:**
| Spec § | 요구사항 | Plan task |
|--------|----------|----------|
| §2 포함 (V1 자산 mv) | Task 1 Step 5 ✅ |
| §2 포함 (web-ai/CLAUDE.md 신규) | Task 1 Step 7 ✅ |
| §2 포함 (web-ai/start.bat 신규) | Task 1 Step 8 ✅ |
| §2 범위 외 (Python import 변경 없음) | Task 1 Step 10 의 grep 으로 검증 ✅ |
| §3.3 web-ai/CLAUDE.md 정확한 내용 | Task 1 Step 7 — 동일 markdown 본문 포함 ✅ |
| §3.3 web-ai/start.bat 정확한 내용 | Task 1 Step 8 — 동일 bat 본문 포함 ✅ |
| §3.4 load_dotenv 경로 갱신 | Task 1 Step 2 (grep) + Step 6 (갱신) ✅ |
| §4 작업 순서 (사전 검토 → mv → 검증 → push → 사용자 검증) | Task 1 Step 1-13 + Task 2 ✅ |
| §5 위험 (.env 로드 실패, 자동매매 중단 등) | Task 2 Step 1 의 first-start verification + load_dotenv 명시 ✅ |
| §6.1 자동 검증 (pytest + grep) | Task 1 Step 3 (baseline) + Step 11 (post-mv) + Step 10 (grep) ✅ |
| §6.2 수동 검증 (start.bat + /status + 30분 관측) | Task 2 Step 1-3 ✅ |
| §8 DoD 8 항목 | 전체 (Task 1 + Task 2 합) ✅ |
No gaps.
**2. Placeholder scan:** No "TBD" / "implement later". load_dotenv 갱신은 Step 2 grep 결과에 의존하지만, Step 6 에 정확한 갱신 패턴 (2 코드 예시) 포함 — placeholder 아님.
**3. Type consistency:** N/A (refactor only, 새 함수/타입 0). 모든 step 의 명령어와 파일 경로 일관 — `signal_v1/` 표기 + `web-ai/` 표기 통일.
Plan passes self-review.

View File

@@ -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: <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 자동화 — 일정 운영 안정화 후

View File

@@ -0,0 +1,267 @@
# web-ai V1 루트 → `signal_v1/` Rename Design
**작성일**: 2026-05-16
**작성자**: gahusb
**상태**: Approved for implementation
**선행 spec**:
- Confidence Signal Pipeline V2 Phase 0 (`2026-05-15-confidence-signal-pipeline-v2-architecture.md`)
- stock-lab → stock graduation (`2026-05-15-stock-lab-rename-to-stock.md`) — 동일 atomic refactor 패턴
---
## 1. 목표
`web-ai/` 디렉토리에 V1 자동매매 시스템 (main_server.py + modules/ + 자체 LSTM + KIS + Telegram Bot) 과 V2 시그널 파이프라인 (`signal_v2/` Phase 2 시작) 이 함께 거주할 예정. V1 자산을 모두 `signal_v1/` 하위로 격리해 신/구 분리 명확.
**Why**: 사용자 명시 ("기존 기능들도 봤을때 헷갈리지 않게 signal_v2에서 사용하는거 아니면 web-ai/signal_v1 으로 몰아넣어줘"). V2 Phase 6 deprecation 시점에 `rm -rf signal_v1/` 단순화. Phase 2 spec 작성 전에 새 이름 `signal_v1/` 기준으로 진행하면 후속 갱신 비용 회피.
본 리네이밍은 **Phase 2 brainstorming 의 도중 분기**한 별도 슬라이스 — stock-lab → stock graduation 과 동일 패턴.
---
## 2. 범위
### 포함
- `git mv` web-ai 루트의 모든 V1 자산을 `signal_v1/` 안으로:
- 진입점: `main_server.py`, `warmup_and_restart.py`, `watchlist_manager.py`, `backtester.py`, `theme_manager.py`, `backtest_runner.py`
- 모듈: `modules/` (전체)
- 데이터: `data/` (전체 — runtime data 보존)
- 테스트: `tests/` (전체)
- 스크립트: `start.bat`
- 문서: `KIS_SETUP.md`, `README.md`, `CLAUDE.md` (기존 V1 가이드)
- 로그: `bot_ipc.json`, `bot_output.log`, `daily_launcher.log`, `server.log`, `telegram_bot.log`, `warmup.log`
- `__pycache__/` (gitignore)
- `web-ai/CLAUDE.md` 신규 — web-ai 루트의 새 가이드 (signal_v1 + signal_v2 디렉토리 안내, 공유 `.env`, Phase 6 deprecation 계획)
- `web-ai/start.bat` 신규 — `cd signal_v1 && python main_server.py` (또는 절대 경로 형태)
- 운영 검증: 자체 자동매매 봇 정상 기동 + Telegram Bot polling + KIS 토큰 로딩
### 범위 외 (NOT)
- Python import 경로 변경 — `signal_v1/` 안에서 진입점 실행 시 cwd 가 `signal_v1/` 이라 기존 `from modules.X` 그대로 작동. import 전면 갱신 불필요.
- `signal_v2/` 디렉토리 생성 — Phase 2 spec 의 작업.
- `.env` 분리 — V1 + V2 환경변수 모두 `web-ai/.env` 한 곳 (signal_v1 의 python 진입점이 cwd 기준 `.env` 로드 시 path 갱신 필요, 단순 조정).
- `.gitignore` — 기존 패턴 그대로 (`signal_v1/__pycache__`, `signal_v1/data/*.db` 등은 일반 패턴으로 커버).
- 다른 lab / web-backend / web-ui 영향 — 0.
- start_signal_v2.bat — Phase 2 spec 의 작업.
---
## 3. 변경 매트릭스
### 3.1 web-ai 루트 (작업 전)
```
web-ai/
├── .env ← 유지
├── .gitignore ← 유지
├── CLAUDE.md ← signal_v1/ 로 mv (현 V1 가이드)
├── KIS_SETUP.md ← signal_v1/ 로 mv
├── README.md ← signal_v1/ 로 mv
├── main_server.py ← signal_v1/ 로 mv
├── warmup_and_restart.py ← signal_v1/ 로 mv
├── watchlist_manager.py ← signal_v1/ 로 mv
├── backtester.py ← signal_v1/ 로 mv
├── backtest_runner.py ← signal_v1/ 로 mv
├── theme_manager.py ← signal_v1/ 로 mv
├── start.bat ← signal_v1/ 로 mv (이후 web-ai/start.bat 신규)
├── modules/ ← signal_v1/ 로 mv
├── data/ ← signal_v1/ 로 mv
├── tests/ ← signal_v1/ 로 mv
├── __pycache__/ ← signal_v1/ 로 mv (gitignore)
├── bot_ipc.json ← signal_v1/ 로 mv
├── bot_output.log ← signal_v1/ 로 mv
├── daily_launcher.log ← signal_v1/ 로 mv
├── server.log ← signal_v1/ 로 mv
├── telegram_bot.log ← signal_v1/ 로 mv
└── warmup.log ← signal_v1/ 로 mv
```
### 3.2 web-ai 루트 (작업 후)
```
web-ai/
├── .env ← 공유 (V1 + V2 변수)
├── .gitignore ← 기존
├── CLAUDE.md ← 신규 (web-ai 레벨 가이드)
├── start.bat ← 신규 (signal_v1 진입)
├── signal_v1/
│ ├── CLAUDE.md ← 기존 V1 가이드 (이동)
│ ├── KIS_SETUP.md
│ ├── README.md
│ ├── main_server.py
│ ├── warmup_and_restart.py
│ ├── ... (이하 모든 V1 자산)
│ ├── start.bat ← 이동본 (사용 안 함, 향후 정리)
│ ├── modules/
│ ├── data/
│ ├── tests/
│ └── (log 파일들)
└── signal_v2/ ← Phase 2 작업 (본 spec 외)
```
### 3.3 신규 파일 2개 — 정확한 내용
**`web-ai/CLAUDE.md` (신규)**:
```markdown
# web-ai — Workspace 가이드
Windows AI 머신 (AMD 9800X3D + RTX 5070 Ti) 의 두 시그널 파이프라인 컨테이너.
## 디렉토리 구조
| 경로 | 역할 | 상태 |
|------|------|------|
| `signal_v1/` | V1 자체 자동매매 시스템 (main_server.py + Trading Bot + Telegram Bot + LSTM + Ollama + KIS 자동주문) | 운영 중. Confidence Signal Pipeline V2 Phase 6 에서 deprecation 예정 |
| `signal_v2/` | V2 신호 파이프라인 (stock pull worker + Chronos-2 + signal API client) | Phase 2 에서 신설 |
| `.env` | V1 + V2 환경변수 공유 | KIS_*, TELEGRAM_*, STOCK_API_URL, WEBAI_API_KEY 등 |
| `start.bat` | V1 진입 (signal_v1 디렉토리 안 main_server.py 실행) | V2 별도 start 스크립트는 signal_v2/start.bat |
## 운영 가이드
- V1 시작: `start.bat` 또는 `cd signal_v1 && python main_server.py`
- V2 시작 (Phase 2 이후): `cd signal_v2 && python -m uvicorn main:app --port 8001`
- 둘 다 동시 실행 가능 (포트 분리: V1=8000, V2=8001)
## Phase 진행 상태 (Confidence Signal Pipeline V2)
`web-ui/docs/superpowers/specs/2026-05-15-confidence-signal-pipeline-v2-architecture.md` 참조.
자세한 V1 가이드는 `signal_v1/CLAUDE.md` 참조.
```
**`web-ai/start.bat` (신규)**:
```bat
@echo off
cd /d "%~dp0\signal_v1"
python main_server.py
```
### 3.4 운영 영향 — `.env` 로드 경로
기존 V1 코드 (`signal_v1/modules/config.py` 등) 는 `load_dotenv()` 호출 시 cwd 또는 절대 경로의 `.env` 를 찾음. cwd 가 `signal_v1/` 이라면 `.env``web-ai/.env` (parent) 이라 못 찾을 수 있음.
**해결**: 진입점 (`signal_v1/main_server.py` 등) 의 `load_dotenv()` 호출에 명시적 경로 추가:
```python
from pathlib import Path
from dotenv import load_dotenv
# web-ai/.env (signal_v1/ 의 parent) 명시 로드
load_dotenv(Path(__file__).parent.parent / ".env")
```
작업 매트릭스:
- `signal_v1/main_server.py``load_dotenv()` 1-2 줄 갱신
- `signal_v1/warmup_and_restart.py` 동일
- `signal_v1/modules/config.py` 같은 환경변수 로딩 위치 점검
---
## 4. 작업 순서
```
1. 사전 검토 (10분)
- web-ai 자체 자동매매 봇 운영 중 → 작업 시간대 결정 (장외: 평일 16:00 이후 / 주말)
- 본 spec §3 매트릭스 모든 파일 grep cross-check
- .env 로드 위치 grep — `load_dotenv` 호출 모두 찾기
- 데이터 파일 (data/, *.log, *.json) 손실 위험 없음 확인 (git mv 는 history 보존)
2. atomic refactor (1 commit)
- mkdir signal_v1
- git mv (위 매트릭스 항목 전부) signal_v1/
- signal_v1/main_server.py 외 .env 로드 위치 갱신
- web-ai/CLAUDE.md 신규
- web-ai/start.bat 신규
3. 로컬 검증 (cwd=signal_v1)
- python -m pytest tests/unit -q (기존 V1 테스트 통과)
- python main_server.py 시작 검증
- .env 로딩 확인 (KIS / Telegram / Ollama 환경변수)
- 봇 정상 시작 → telegram 알림 도착 → /status 응답 → 종료
4. git push (web-ai repo)
- sub Gitea: https://gitea.gahusb.synology.me/gahusb/ai-trade.git
- 본 작업은 NAS deploy 와 무관 (web-ai 는 로컬 Windows 머신).
5. 사용자 수동 검증
- 시장 시작 (다음 평일 09:00) 시점 봇 정상 동작 확인 또는 일/주말 가짜 트리거
```
---
## 5. 위험 및 완화
| 위험 | 완화 |
|------|------|
| `.env` 로드 실패 → KIS 토큰 못 가져옴 → 자동매매 중단 | 진입점 (main_server.py / warmup_and_restart.py) 의 `load_dotenv` 명시 경로 추가. 시작 직후 KIS auth 확인 |
| 자동매매 중 작업 → 거래 중단 | 작업 시간대를 장외 (평일 16:00+ 또는 주말) 로 제한 |
| Python import 회귀 | `signal_v1/` cwd 기준 `from modules.X` 그대로. 외부 import 불필요. 기존 76+ 테스트 통과로 검증 |
| 데이터 파일 (data/models/, data/ensemble_history.json 등) 손실 | git mv 사용 — history 보존, 파일 내용 무변경. 사전 git status 로 dirty 없음 확인 |
| Telegram Bot 중복 polling (이전 프로세스 미종료) | start.bat 재시작 시 main_server.py 의 좀비 정리 로직 자동 동작 |
| .env 의 절대 경로 참조 (e.g. `data/kis_token.json` 같은 상대 경로) | cwd 변경 영향 — 진입점이 working directory 를 `signal_v1/` 으로 설정하면 기존 상대 경로 그대로 작동. start.bat 의 `cd /d "%~dp0\signal_v1"` 가 보장 |
| 향후 web-ai 레벨 외부 호출 (e.g. agent-office → web-ai :8000) | V1 main_server.py 는 port 8000 유지. URL 변경 없음. |
| signal_v2 진입점이 signal_v1 의 IPC 와 충돌 | Phase 2 가 별도 port :8001 + 별도 디렉토리. IPC SharedMemory 이름 분리 (V1 의 `web_ai_bot_ipc` 그대로 유지, V2 는 IPC 사용 안 함) |
---
## 6. 테스트 / 검증
### 6.1 자동
```bash
# V1 테스트 전체 통과
cd /c/Users/jaeoh/Desktop/workspace/web-ai/signal_v1
python -m pytest tests/unit -q
# Expected: 기존 PASS 개수 그대로
# stock-lab → stock 의 잔여 참조 패턴 검증과 동일 — V1 안에서 import 회귀 없음
grep -rn "from web-ai" /c/Users/jaeoh/Desktop/workspace/web-ai/signal_v1
# Expected: 0 lines (없어야 함)
```
### 6.2 수동
- `cd web-ai && start.bat` (또는 `cd web-ai/signal_v1 && python main_server.py`)
- 콘솔 로그에 KIS 인증 성공 / Telegram Bot connected / Ollama 모델 로드 확인
- Telegram /status 명령 → 정상 응답
- 30분 관측 후 Watchdog 정상 (자식 프로세스 healthy)
---
## 7. 운영 영향
| 항목 | 영향 |
|------|------|
| 다운타임 | 작업 시간 + 첫 시작 검증 = ~30분 |
| 사용자 영향 | V1 자동매매 봇 일시 중단 (장외 시간대 진행 권장) |
| `.env` 갱신 | 없음 (위치 그대로, 진입점만 명시 경로 변경) |
| frontend 영향 | 없음 |
| 다른 lab / web-backend | 없음 (web-ai 외부 의존 0) |
| Gitea push | web-ai repo 만 |
---
## 8. 완료 조건 (DoD)
- [ ] `web-ai/signal_v1/` 디렉토리 신설 + 매트릭스의 모든 V1 자산 mv 완료 (git history 보존)
- [ ] `web-ai/CLAUDE.md` 신규 (web-ai 레벨 가이드)
- [ ] `web-ai/start.bat` 신규 (signal_v1 cd 후 main_server.py)
- [ ] `signal_v1/main_server.py`, `warmup_and_restart.py` 등의 `load_dotenv()``web-ai/.env` 를 명시 로드
- [ ] `signal_v1/tests/unit/` 전체 pytest 통과 (기존 baseline 그대로)
- [ ] `cd web-ai && start.bat` 으로 V1 봇 정상 시작 + Telegram /status 응답
- [ ] grep `from web-ai\.` 또는 `from web-ai/` 결과 0 lines
- [ ] web-ai repo push 완료 (단일 commit)
---
## 9. Phase 2 와의 관계
본 리네이밍 완료 후 즉시 **Phase 2 brainstorming 재개**. Phase 2 spec 은:
- 새 이름 `web-ai/signal_v2/` 기준
- Phase 2 의 모든 결정 (배치 = 별도 FastAPI app :8001 / scope = 3 항목 / scheduler = asyncio cron / client = httpx + 자체 retry / rate limit = SQLite / test = pytest-asyncio) 그대로 반영
- 디자인 섹션 1 (목표/scope) + 섹션 2 (파일 구조 = web-ai/signal_v2/) 의 검토 완료 상태에서 섹션 3-7 진행
```
[본 리네이밍 spec/plan/실행] → [Phase 2 spec 작성 재개]
~30분-1시간 ~15분 (남은 섹션)
```