refactor(web-ai): rename signal_v2→ai_trade, deprecate signal_v1
박재오 결정 2026-05-19 — V2를 정식 명칭 ai_trade로 graduation, V1은 deprecated 마킹 (legacy 디렉토리 이동은 file lock 풀린 후 후속). 변경 사항: - signal_v2/ → ai_trade/ (git mv, import 일괄 sed: signal_v2.x → ai_trade.x) - root start.bat → legacy/start_v1.bat (V1 자동 시작 차단) - ai_trade/start.bat 내부 uvicorn target signal_v2.main → ai_trade.main - signal_v1/DEPRECATED.md 추가 (사용 금지 명시) - CLAUDE.md 디렉토리 표·서버 시작 방식 갱신 - services/ 디렉토리 미래 예정 (Plan-B-Insta 작업 시 신설) ai_trade tests 59/59 PASS 확인. signal_v1/ 디렉토리 자체 이동(legacy/signal_v1/)은 telegram_bot.log + data/news_snapshots.db file lock으로 보류. lock 해제 후 후속 커밋. 후속 작업: Plan-B-Insta (services/insta-render + NAS insta 분할) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -47,11 +47,11 @@ daily_trade_history.json
|
||||
watchlist.json
|
||||
bot_ipc.json
|
||||
|
||||
# Test (top-level only; signal_v2/tests tracked separately)
|
||||
# Test (top-level only; ai_trade/tests tracked separately)
|
||||
tests/
|
||||
tests/*
|
||||
!signal_v2/tests/
|
||||
!signal_v2/tests/**
|
||||
!ai_trade/tests/
|
||||
!ai_trade/tests/**
|
||||
|
||||
# System
|
||||
Thumbs.db
|
||||
@@ -63,5 +63,5 @@ KIS_SETUP.md
|
||||
.claude/
|
||||
|
||||
# Signal V2 runtime data
|
||||
signal_v2/data/*.db
|
||||
signal_v2/data/*.db-*
|
||||
ai_trade/data/*.db
|
||||
ai_trade/data/*.db-*
|
||||
|
||||
277
CHECK_POINT.md
Normal file
277
CHECK_POINT.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# web-ai CHECK_POINT
|
||||
|
||||
> Windows AI Server (192.168.45.59), AMD 9800X3D + RTX 5070 Ti (16GB VRAM).
|
||||
> V1(LSTM 레거시) + V2(Chronos-2 signal pipeline) 이중 구조.
|
||||
> 2026-05-18 작성.
|
||||
|
||||
## 🚀 2026-05-18 박재오 7 결정 — Windows 컴퓨팅 노드 신설 (1주 작업)
|
||||
|
||||
박재오 결정 7건 완료. 상세 가이드: `Obsidian Vault/raw/2026-05-18-Windows-NAS-아키텍처-7결정-통합.md`
|
||||
|
||||
### 결정 6 — 옵션 4 하이브리드 운영
|
||||
|
||||
```
|
||||
[Windows AI Server]
|
||||
|
||||
🔵 Native Python (NSSM 자동 시작, HIGH priority)
|
||||
├─ ai_trade (트레이딩 :8001) ⭐ 절대 우선
|
||||
├─ Ollama qwen3:14b (:11435)
|
||||
└─ MusicGen (:8765)
|
||||
|
||||
🟢 WSL2 + Docker Engine (Docker Desktop X, 라이선스·메모리 ↓)
|
||||
├─ insta-render (:18710) ⭐ NEW
|
||||
├─ music-render (:8771) ⭐ NEW
|
||||
├─ video-render (:18712) ⭐ NEW (외부 API 게이트웨이)
|
||||
└─ task-watcher (옵션 d 작업 감지)
|
||||
```
|
||||
|
||||
### Day 2 — WSL2 + Docker Engine 설치 ⭐ (2시간)
|
||||
|
||||
```powershell
|
||||
# 관리자 PowerShell
|
||||
wsl --install -d Ubuntu-22.04
|
||||
# 재부팅 후 Ubuntu 초기 설정
|
||||
|
||||
wsl -d Ubuntu-22.04
|
||||
# 안에서 Docker Engine 설치
|
||||
sudo apt update && sudo apt install -y ca-certificates curl gnupg
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list
|
||||
sudo apt update && sudo apt install -y docker-ce docker-compose-plugin
|
||||
sudo usermod -aG docker $USER
|
||||
sudo systemctl enable docker
|
||||
sudo systemctl start docker
|
||||
|
||||
# Tailscale (NAS와 같은 LAN 확인)
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
sudo tailscale up
|
||||
```
|
||||
|
||||
- [ ] WSL2 + Ubuntu 22.04 설치
|
||||
- [ ] Docker Engine 설치 (Desktop X)
|
||||
- [ ] Tailscale 가입
|
||||
|
||||
### Day 3~4 — insta-render 컨테이너 신설 ⭐ (4시간)
|
||||
|
||||
**디렉토리**: `C:\Users\jaeoh\Desktop\workspace\web-ai-services\insta-render\`
|
||||
|
||||
**Dockerfile**: Playwright + Python 3.12 (raw 파일에 풀 코드)
|
||||
|
||||
**main.py**: Redis 큐 worker + Playwright Browser pool + Semaphore(1)
|
||||
|
||||
- [ ] 디렉토리 생성 + Dockerfile + main.py + requirements.txt
|
||||
- [ ] `docker compose up -d insta-render`
|
||||
- [ ] 테스트: NAS Redis에서 작업 push → Windows에서 처리 확인
|
||||
|
||||
### Day 6 — NSSM 자동 시작 ⭐ (1시간)
|
||||
|
||||
**NSSM 다운로드**: https://nssm.cc/download
|
||||
|
||||
```powershell
|
||||
# 관리자 PowerShell
|
||||
|
||||
# 트레이딩 자동 시작 (HIGH priority — 결정 b)
|
||||
nssm install ai_trade "C:\Python312\python.exe" "-m uvicorn main:app --host 0.0.0.0 --port 8001"
|
||||
nssm set ai_trade AppDirectory "C:\Users\jaeoh\Desktop\workspace\web-ai\ai_trade"
|
||||
nssm set ai_trade Priority HIGH_PRIORITY_CLASS
|
||||
nssm set ai_trade AppStartup AUTO
|
||||
|
||||
# WSL2 + Docker 자동 시작
|
||||
nssm install wsl_docker "wsl" "-d Ubuntu-22.04 -- sudo service docker start && cd /workspace/web-ai-services && docker compose up -d"
|
||||
nssm set wsl_docker AppStartup AUTO
|
||||
|
||||
# 시작
|
||||
nssm start ai_trade
|
||||
nssm start wsl_docker
|
||||
```
|
||||
|
||||
- [ ] NSSM 다운로드 + 압축 해제 + PATH 추가
|
||||
- [ ] ai_trade service 등록 (HIGH priority)
|
||||
- [ ] wsl_docker service 등록
|
||||
- [ ] 재부팅 후 자동 시작 확인
|
||||
|
||||
### Day 7 — 작업 감지 (옵션 d) — task-watcher ⭐ (2시간)
|
||||
|
||||
박재오 작업 중 → Redis `queue:paused` 플래그 → 워커 일시정지. 트레이딩은 영향 X.
|
||||
|
||||
선택지:
|
||||
- **A. 자동 (Python pynput + PowerShell API)** — 마우스·게임 process 감지, 자동 토글
|
||||
- **B. 수동 토글** — 박재오님이 작업 시작 시 `redis-cli SET queue:paused 1`, 종료 시 `DEL`
|
||||
- **C. NAS frontend에 토글 UI 1개** — 클릭 한 번
|
||||
|
||||
→ 시작은 **B 수동 토글** (구현 0, 즉시 가능), 나중에 A 자동화로 진화.
|
||||
|
||||
- [ ] B 수동 토글 명령어 확인
|
||||
- [ ] 또는 A Python pynput 자동 감지 구현 (선택, 2시간)
|
||||
|
||||
### Day 5 — music-render 컨테이너 (선택 — MusicGen 패턴 정착)
|
||||
|
||||
기존 NAS music-lab → Windows MusicGen 호출 패턴 이미 운영 중. 표준화만:
|
||||
- [ ] Redis 큐 사용으로 전환 (HTTP 직접 호출 X)
|
||||
- [ ] Browser pool 같은 패턴 적용 (Suno + MusicGen 동시 1개)
|
||||
|
||||
### Day 5 — video-render 컨테이너 (선택 — 영상 생성 결정 4)
|
||||
|
||||
외부 영상 API 6개 게이트웨이 (Runway·Sora·Veo·Pika·Kling·Luma):
|
||||
- [ ] 박재오 자금·품질 판단 후 1~2개 가입
|
||||
- [ ] `.env`에 API 키 추가
|
||||
- [ ] video-render Docker 컨테이너 신설
|
||||
|
||||
---
|
||||
|
||||
## 🔥 2026-05-18 추가 — NAS API 부하 진짜 원인 발견
|
||||
|
||||
박재오 발견: 5건 + 중기 2건 적용 후 **web-ai V1+V2 4 process 종료가 NAS CPU 가장 큰 즉시 감소**.
|
||||
→ 진짜 병목은 **web-ai → NAS stock(:18500) 인바운드 API 호출 빈도**.
|
||||
|
||||
상세: `Obsidian Vault/raw/2026-05-18-NAS-Window-AI-API-부하-해결방안.md`
|
||||
|
||||
### 🔴 추가 즉시 작업 (50분으로 70% 부담 감소)
|
||||
|
||||
#### A. 캐시 TTL 대폭 증가 ⭐⭐⭐ (10분)
|
||||
**파일**: `ai_trade/stock_client.py`
|
||||
```python
|
||||
# 변경 전 → 변경 후
|
||||
PORTFOLIO_TTL = 60 → 180 # 3분
|
||||
NEWS_TTL = 300 → 600 # 10분
|
||||
SCREENER_TTL = 60 → 300 # 5분
|
||||
```
|
||||
- [ ] 3 TTL 상수 증가
|
||||
- 효과: 분당 12 호출 → 3~4 호출 (70% 감소)
|
||||
|
||||
#### B. V1·V2 단일화 결정 ⭐⭐ (10분 결정)
|
||||
- 동시 운영 시 NAS API 부담 2배 + KIS rate limit 충돌
|
||||
- **권장**: V1 폐기 + V2 단독 (Phase 4 자산 활용)
|
||||
- 또는 V2 임시 종료 + V1 유지 → Phase 5 진입 시 V1 폐기
|
||||
- [ ] 박재오 결정
|
||||
- [ ] 선택 안 된 쪽 `legacy/` 또는 `.disabled`
|
||||
- [ ] start.bat 한 쪽만
|
||||
|
||||
#### C. (NAS 측) stock TTLCache — web-backend CHECK_POINT.md #13 참고
|
||||
- 박재오가 web-backend/stock에서 별도 적용
|
||||
|
||||
## 🟢 현재 상태
|
||||
|
||||
- **V2 Phase 4 완료** (5/17, 56/56 tests pass, main push)
|
||||
- Chronos-2 zero-shot 1일 수익률 + 분봉 모멘텀 5단계
|
||||
- 09:00~15:30 매 1분 / 16:00 일봉 추론
|
||||
- Sell-first 우선순위 (stop_loss · anomaly · take_profit) + 매수 hard gate
|
||||
- Confidence = chronos×0.5 + momentum×0.3 + screener×0.2
|
||||
|
||||
---
|
||||
|
||||
## 🔴 즉시 (이번 주)
|
||||
|
||||
### 1. V1 vs V2 KIS rate limit 충돌 해결
|
||||
- **현재**: V1 + V2 동시 실행 시 KIS EGW00201 (초당 2회 제한) 충돌
|
||||
- **임시 해결**: V2 종료 상태 (현재 V1만 운영 중)
|
||||
- **결정 필요**: V1 deprecation 시점 (Phase 6)
|
||||
- [ ] 박재오 결정 — V1 폐기 일자 (예: 5/20 / 5/31)
|
||||
- [ ] Phase 5 진입 전 V1 정리
|
||||
|
||||
### 2. `.venv` 한글 경로 문제 해결
|
||||
- 가상환경 한글 경로 깨짐 → 시스템 Python 사용 강제
|
||||
- 다른 개발 머신 협업 시 문제
|
||||
- [ ] `.venv`를 영문 경로로 이전 또는 시스템 Python으로 통일
|
||||
- [ ] start.bat에 경로 명시
|
||||
|
||||
### 3. `state.signals` consumer-drain protocol 정의
|
||||
- Phase 5 prereq
|
||||
- 신호가 누적되기만 하고 소비 로직 미정의
|
||||
- [ ] consumer (예: agent-office /signal endpoint)가 처리한 신호 marking
|
||||
- [ ] 24h 만료 dedup 외에 *처리됨* 상태 추가
|
||||
|
||||
---
|
||||
|
||||
## 🟡 중기 (1~2주, Phase 5)
|
||||
|
||||
### 4. agent-office `/signal` 엔드포인트 통합
|
||||
- web-ai V2가 매수/매도 신호 생성 → agent-office로 push
|
||||
- agent-office가 텔레그램 발송 + 사용자 결정 대기
|
||||
- [ ] V2에서 agent-office HTTP POST 호출 추가
|
||||
- [ ] payload: ticker, action, confidence, reasoning, chronos_quantile
|
||||
|
||||
### 5. Ollama Qwen3 14B 통합 (Windows 로컬)
|
||||
- 신호 *해석* 레이어 — 단순 규칙 결과 → LLM 자연어 설명
|
||||
- 9.3GB VRAM 사용 (Chronos 7GB와 동시 가능, 15.5GB)
|
||||
- [ ] Ollama Windows 설치 (이미 실행 중인지 확인)
|
||||
- [ ] `state.signals` 큐에서 신호 pop → Qwen3 prompt → 결과 add
|
||||
- [ ] 텔레그램 전송 시 LLM 해석 텍스트 포함
|
||||
|
||||
### 6. 이중 텔레그램 전송
|
||||
- 현재: V1만 텔레그램 발송 (Telegram Bot + KIS 자동주문)
|
||||
- Phase 5: V2도 별도 chat_id로 발송 (박재오 본인용 + 검증용)
|
||||
- [ ] V2 텔레그램 chat_id 환경변수 (`TELEGRAM_V2_CHAT_ID`)
|
||||
- [ ] V1·V2 메시지 톤 차별화 (V1 = 자동주문 / V2 = 신호 알림)
|
||||
|
||||
### 7. holidays.json 자동 동기화
|
||||
- 현재: NAS에서 수동 copy
|
||||
- 한국 휴장일 누락 시 V2 폴링 실수 (휴장일에도 KIS 호출)
|
||||
- [ ] NAS realestate-lab 또는 별도 컨테이너에서 휴장일 자동 발급
|
||||
- [ ] V2가 부팅 시·매일 00:00에 GET 갱신
|
||||
|
||||
---
|
||||
|
||||
## 🟢 장기 (1개월+, Phase 6+)
|
||||
|
||||
### 8. V1 완전 deprecation
|
||||
- LSTM 7-feature 모델 + main_server.py 폐기
|
||||
- 모든 자동매매 V2로 통일
|
||||
- [ ] V1 종료 일자 박재오 결정
|
||||
- [ ] V1 코드 `legacy/` 폴더로 이동 (read-only)
|
||||
|
||||
### 9. Chronos-2 모델 미세조정 검토
|
||||
- 현재 zero-shot. 한국 주식 데이터로 미세조정 시 정확도 ↑ 가능?
|
||||
- 박재오 자체 학습 데이터 (KIS 1년치) → finetune
|
||||
- [ ] 데이터 수집·전처리
|
||||
- [ ] LoRA 또는 full finetune 결정
|
||||
|
||||
### 10. KIS WebSocket 실 운영 검증
|
||||
- 현재 코드는 있으나 검증 부족
|
||||
- 실시간 호가가 1분 폴링보다 빠른 신호 확보 가능
|
||||
- [ ] 1주일 운영 후 latency·드롭 측정
|
||||
|
||||
---
|
||||
|
||||
## ✅ 최근 완료 (참고)
|
||||
|
||||
- 2026-05-17: emit/skip 로깅 추가 (`2aa9f48`)
|
||||
- 2026-05-17: signal_generator poll_loop 통합 — Phase 4 완료 (`cc6310d`)
|
||||
- 2026-05-16: 코드 리뷰 수정 — sell-first 순서, anomaly 테스트 (`e574074`)
|
||||
- 2026-05-16: signal_generator 초안 — 9개 unit test (`b9def06`)
|
||||
- 2026-05-15: Foundation — 6개 env 임계값 + state.signals 필드 (`05ab284`)
|
||||
- 2026-05-14: FP32 강제 — Chronos FP16 overflow 회피 (`760f914`)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 운영 커맨드 (Windows PowerShell)
|
||||
|
||||
```powershell
|
||||
# V2 시작
|
||||
cd C:\Users\jaeoh\Desktop\workspace\web-ai\ai_trade
|
||||
.\start.bat
|
||||
|
||||
# 테스트 실행 (56 tests)
|
||||
pytest tests/ -v
|
||||
|
||||
# 로그 확인 (V2 실시간)
|
||||
Get-Content logs/ai_trade.log -Wait
|
||||
|
||||
# Chronos 메모리 사용 확인
|
||||
nvidia-smi
|
||||
|
||||
# KIS API 헬스 (REST)
|
||||
curl http://localhost:8001/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고
|
||||
|
||||
- 위키: [[자산-주식-자동매매]] (V3.1 → V2 Phase 4 정정 필요)
|
||||
- NAS stock 연동: `192.168.45.54:18500` (X-WebAI-Key 인증)
|
||||
- CLAUDE.md (본 디렉토리 루트): Phase 진행도 + 환경변수 명세
|
||||
|
||||
## 변경 이력
|
||||
|
||||
- 2026-05-18: 페이지 신설. 즉시 3건 (V1/V2 충돌, .venv 한글, consumer protocol) + 중기 4건 (Phase 5) + 장기 3건 (V1 deprecation·Chronos finetune·WebSocket).
|
||||
32
CLAUDE.md
32
CLAUDE.md
@@ -11,29 +11,27 @@ Windows AI 머신 (AMD 9800X3D + RTX 5070 Ti 16GB) 의 두 신호 파이프라
|
||||
|
||||
| 경로 | 역할 | 포트 | 상태 |
|
||||
|------|------|------|------|
|
||||
| `signal_v1/` | 레거시 자동매매 시스템 (LSTM 7-features + Gemini Flash + Telegram Bot + KIS 자동주문) | `:8000` | 운영 중. **V2 Phase 6 에서 deprecation 예정** |
|
||||
| `signal_v2/` | Confidence Signal Pipeline V2 (Chronos-bolt + 분봉 모멘텀 + KIS WebSocket + 신호 생성) | `:8001` | **Phase 4 완료 (2026-05-17)**, Phase 5 대기 |
|
||||
| `.env` | V1 + V2 환경변수 공유 | — | `KIS_REAL_*`, `TELEGRAM_*`, `STOCK_API_URL`, `WEBAI_API_KEY`, `LOG_LEVEL` |
|
||||
| `start.bat` | V1 진입점 | — | `signal_v1/main_server.py` 실행 |
|
||||
| `signal_v2/start.bat` | V2 진입점 | — | `signal_v2/main.py` uvicorn 실행 |
|
||||
| `signal_v1/` | ⚠️ **DEPRECATED 2026-05-19** — 레거시 LSTM 봇. 사용 안 함. 향후 `legacy/signal_v1/`로 이동 예정 (현재 file lock 풀린 후) | `:8000` | **OFF** |
|
||||
| `ai_trade/` | 자동매매 메인 (구 `signal_v2` 2026-05-19 rename) — Chronos-bolt + 분봉 모멘텀 + KIS WebSocket + 신호 생성 | `:8001` | **Phase 4 완료 (2026-05-17)**, Phase 5 대기 |
|
||||
| `legacy/start_v1.bat` | (deprecated) V1 진입점 — root `start.bat`에서 이동됨. 자동 실행 차단 | — | **OFF** |
|
||||
| `ai_trade/start.bat` | 자동매매 진입점 | — | `ai_trade/main.py` uvicorn 실행 |
|
||||
| `services/` | (예정) NAS↔Windows 분산 worker — insta-render·music-render·video-render·task-watcher | 18710~ | **Plan-B-Insta 작업 중** |
|
||||
| `.env` | 환경변수 (`KIS_REAL_*`, `TELEGRAM_*`, `STOCK_API_URL`, `WEBAI_API_KEY`, `LOG_LEVEL`) | — | |
|
||||
| `requirements.txt` | 공용 의존성 | — | torch, chronos-forecasting, fastapi, httpx, websockets 등 |
|
||||
|
||||
`.venv` 는 **구조적으로 깨짐**: `pyvenv.cfg` 가 한글 사용자 경로(`C:\Users\박재오\...`) 를 포함하여 콘솔 코드페이지가 roundtrip 못함. 테스트는 시스템 Python 으로 실행: `C:\Users\jaeoh\AppData\Local\Programs\Python\Python312\python.exe -m pytest signal_v2/tests -q`.
|
||||
`.venv` 는 **구조적으로 깨짐**: `pyvenv.cfg` 가 한글 사용자 경로(`C:\Users\박재오\...`) 를 포함하여 콘솔 코드페이지가 roundtrip 못함. 테스트는 시스템 Python 으로 실행: `C:\Users\jaeoh\AppData\Local\Programs\Python\Python312\python.exe -m pytest ai_trade/tests -q`.
|
||||
|
||||
---
|
||||
|
||||
## 서버 시작 방식
|
||||
|
||||
### V1 단독 (운영 기본)
|
||||
```bat
|
||||
cd C:\Users\jaeoh\Desktop\workspace\web-ai
|
||||
.\start.bat
|
||||
```
|
||||
기대 로그: `[Bot] Cycle Start ...`, `[AI] 005930: NN epochs ...`, `[Ensemble] tech=... news=... lstm=...`, `Score: 0.xx [HOLD]`
|
||||
### V1 (⚠️ DEPRECATED — 운영 X)
|
||||
2026-05-19부터 자동 시작 차단. `legacy/start_v1.bat`에 보존 (참고용만).
|
||||
별도 backtest 등 1회성 시 필요 시 박재오 직접 `legacy/start_v1.bat` 실행.
|
||||
|
||||
### V2 단독 (smoke/검증)
|
||||
### ai_trade 단독 (smoke/검증)
|
||||
```bat
|
||||
cd C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2
|
||||
cd C:\Users\jaeoh\Desktop\workspace\web-ai\ai_trade
|
||||
.\start.bat
|
||||
```
|
||||
기대 로그: `Uvicorn running on http://0.0.0.0:8001`, `poll_loop started`, `[KIS] minute bars ... OK`, `[Chronos] predicted N tickers`, `signal emit XXXXXX buy conf=0.xxx`.
|
||||
@@ -52,7 +50,7 @@ cd C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2
|
||||
| 0 | Architecture & contract spec | ✅ Chronos-2 + Qwen3 14B 채택 |
|
||||
| 1 | stock 백엔드 WebAI API 보강 (NAS) | ✅ 102/102 tests, 운영 배포 |
|
||||
| 1.5 | V1 → `signal_v1/` rename | ✅ V1 정상 기동 |
|
||||
| 2 | signal_v2 pull worker + signal API client + scheduler | ✅ 19/19 tests, `:8001` 기동 |
|
||||
| 2 | ai_trade pull worker + signal API client + scheduler | ✅ 19/19 tests, `:8001` 기동 |
|
||||
| 3a | KIS REST 분봉 + WebSocket 호가 + NXT 스케줄 | ✅ 33/33 tests |
|
||||
| 3b | Chronos-bolt-base 추론 + 5분봉 모멘텀 분류기 | ✅ 45/45 tests, 실 KIS+Chronos chain 검증 |
|
||||
| 4 | Signal Generator (매수/매도 룰) + pull_worker 통합 + 로깅 | ✅ **2026-05-17 완료, 56/56 tests, push 완료** |
|
||||
@@ -64,7 +62,7 @@ cd C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2
|
||||
|
||||
---
|
||||
|
||||
## signal_v2 디렉토리 내부
|
||||
## ai_trade 디렉토리 내부
|
||||
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
@@ -133,7 +131,7 @@ cd C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2
|
||||
|
||||
## 양쪽 디렉토리 (web-ui ↔ web-ai) 작업 시 주의
|
||||
|
||||
- **코드**: signal_v2 는 web-ai/, spec/plan/메모리는 web-ui/
|
||||
- **코드**: ai_trade 는 web-ai/, spec/plan/메모리는 web-ui/
|
||||
- **커밋**: `web-ai` 와 `web-ui` 는 **별도 Gitea 저장소**. 각각 경로에서만 `git add/commit/push`
|
||||
- **메모리**: Claude Code 의 auto-memory 는 디렉토리별 격리. 핵심 reference 는 양쪽에 미러됨 (`./memory-mirror/` 또는 `~/.claude/projects/C--Users-jaeoh-Desktop-workspace-web-ai/memory/`)
|
||||
- **spec amendment 발생 시**: 코드는 `web-ai` 에 commit, spec 갱신은 `web-ui/docs/superpowers/specs/` 에 commit (Phase 4 spread formula 변경 사례 = web-ui commit `534ded5`)
|
||||
|
||||
@@ -18,7 +18,7 @@ class Settings:
|
||||
)
|
||||
port: int = field(default_factory=lambda: int(os.getenv("SIGNAL_V2_PORT", "8001")))
|
||||
db_path: Path = field(
|
||||
default_factory=lambda: Path(__file__).parent / "data" / "signal_v2.db"
|
||||
default_factory=lambda: Path(__file__).parent / "data" / "ai_trade.db"
|
||||
)
|
||||
# KIS — V1 호환 패턴 (KIS_ENV_TYPE virtual/real)
|
||||
kis_env_type: str = field(default_factory=lambda: os.getenv("KIS_ENV_TYPE", "virtual").lower())
|
||||
@@ -6,14 +6,14 @@ from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from signal_v2 import state as state_mod
|
||||
from signal_v2.chronos_predictor import ChronosPredictor
|
||||
from signal_v2.config import get_settings
|
||||
from signal_v2.kis_client import KISClient
|
||||
from signal_v2.kis_websocket import KISWebSocket
|
||||
from signal_v2.pull_worker import poll_loop, make_asking_price_callback
|
||||
from signal_v2.rate_limit import SignalDedup
|
||||
from signal_v2.stock_client import StockClient
|
||||
from ai_trade import state as state_mod
|
||||
from ai_trade.chronos_predictor import ChronosPredictor
|
||||
from ai_trade.config import get_settings
|
||||
from ai_trade.kis_client import KISClient
|
||||
from ai_trade.kis_websocket import KISWebSocket
|
||||
from ai_trade.pull_worker import poll_loop, make_asking_price_callback
|
||||
from ai_trade.rate_limit import SignalDedup
|
||||
from ai_trade.stock_client import StockClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -5,12 +5,12 @@ import logging
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from signal_v2.kis_client import KISClient
|
||||
from signal_v2.scheduler import (
|
||||
from ai_trade.kis_client import KISClient
|
||||
from ai_trade.scheduler import (
|
||||
KST, _is_market_day, _is_polling_window, _next_interval, _is_post_close_trigger,
|
||||
)
|
||||
from signal_v2.state import PollState
|
||||
from signal_v2.stock_client import StockClient
|
||||
from ai_trade.state import PollState
|
||||
from ai_trade.stock_client import StockClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,7 +45,7 @@ async def poll_loop(
|
||||
# Phase 4: generate signals
|
||||
if dedup is not None and settings is not None:
|
||||
try:
|
||||
from signal_v2.signal_generator import generate_signals
|
||||
from ai_trade.signal_generator import generate_signals
|
||||
generate_signals(state, dedup, settings)
|
||||
except Exception:
|
||||
logger.exception("generate_signals failed")
|
||||
@@ -186,7 +186,7 @@ async def _run_post_close_cycle(kis_client, chronos, state) -> None:
|
||||
|
||||
def update_minute_momentum_for_all(state) -> None:
|
||||
"""매 분봉 cycle 후 호출 — 모든 종목 모멘텀 갱신."""
|
||||
from signal_v2.momentum_classifier import classify_minute_momentum
|
||||
from ai_trade.momentum_classifier import classify_minute_momentum
|
||||
now_iso = datetime.now(KST).isoformat()
|
||||
for ticker, bars in state.minute_bars.items():
|
||||
state.minute_momentum[ticker] = classify_minute_momentum(bars)
|
||||
3
ai_trade/start.bat
Normal file
3
ai_trade/start.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
cd /d "%~dp0\.."
|
||||
python -m uvicorn ai_trade.main:app --host 0.0.0.0 --port 8001
|
||||
BIN
ai_trade/tests/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ai_trade/tests/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ai_trade/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc
Normal file
BIN
ai_trade/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
"""Pytest fixtures for signal_v2 tests."""
|
||||
"""Pytest fixtures for ai_trade tests."""
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -8,7 +8,7 @@ import respx
|
||||
@pytest.fixture
|
||||
def tmp_dedup_db(tmp_path) -> Path:
|
||||
"""SQLite 단위 테스트용 임시 DB path."""
|
||||
return tmp_path / "test_signal_v2.db"
|
||||
return tmp_path / "test_ai_trade.db"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -39,7 +39,7 @@ def test_predict_batch_returns_prediction_dict(mock_pipeline, mock_torch_cpu):
|
||||
quantiles = _mk_quantiles_tensor(101.5, 102.0, 102.5) # narrow around 102
|
||||
mock_pipeline.predict_quantiles.return_value = (quantiles, None)
|
||||
|
||||
from signal_v2.chronos_predictor import ChronosPredictor, ChronosPrediction
|
||||
from ai_trade.chronos_predictor import ChronosPredictor, ChronosPrediction
|
||||
predictor = ChronosPredictor(model_name="mock-model")
|
||||
daily = {"005930": _daily_ohlcv([100] * 60)}
|
||||
result = predictor.predict_batch(daily)
|
||||
@@ -57,7 +57,7 @@ def test_conf_high_when_distribution_narrow(mock_pipeline, mock_torch_cpu):
|
||||
quantiles = _mk_quantiles_tensor(101.99, 102.0, 102.01)
|
||||
mock_pipeline.predict_quantiles.return_value = (quantiles, None)
|
||||
|
||||
from signal_v2.chronos_predictor import ChronosPredictor
|
||||
from ai_trade.chronos_predictor import ChronosPredictor
|
||||
predictor = ChronosPredictor(model_name="mock-model")
|
||||
daily = {"005930": _daily_ohlcv([100] * 60)}
|
||||
result = predictor.predict_batch(daily)
|
||||
@@ -72,7 +72,7 @@ def test_conf_low_when_distribution_wide(mock_pipeline, mock_torch_cpu):
|
||||
quantiles = _mk_quantiles_tensor(70.0, 100.0, 130.0)
|
||||
mock_pipeline.predict_quantiles.return_value = (quantiles, None)
|
||||
|
||||
from signal_v2.chronos_predictor import ChronosPredictor
|
||||
from ai_trade.chronos_predictor import ChronosPredictor
|
||||
predictor = ChronosPredictor(model_name="mock-model")
|
||||
daily = {"005930": _daily_ohlcv([100] * 60)}
|
||||
result = predictor.predict_batch(daily)
|
||||
@@ -84,7 +84,7 @@ def test_return_computed_from_price_relative_to_last_close(mock_pipeline, mock_t
|
||||
quantiles = _mk_quantiles_tensor(109.0, 110.0, 111.0)
|
||||
mock_pipeline.predict_quantiles.return_value = (quantiles, None)
|
||||
|
||||
from signal_v2.chronos_predictor import ChronosPredictor
|
||||
from ai_trade.chronos_predictor import ChronosPredictor
|
||||
predictor = ChronosPredictor(model_name="mock-model")
|
||||
# last close = 100
|
||||
daily = {"005930": _daily_ohlcv(list(range(41, 101)))} # last = 100
|
||||
@@ -6,7 +6,7 @@ import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from signal_v2.kis_client import KISClient
|
||||
from ai_trade.kis_client import KISClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -7,7 +7,7 @@ import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from signal_v2.kis_websocket import KISWebSocket
|
||||
from ai_trade.kis_websocket import KISWebSocket
|
||||
|
||||
|
||||
BASE_REST = "https://openapivts.koreainvestment.com:29443"
|
||||
@@ -10,9 +10,9 @@ def test_health_endpoint_returns_status_online(monkeypatch):
|
||||
monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
|
||||
# Reload modules so they pick up the new env
|
||||
import importlib
|
||||
from signal_v2 import config as cfg
|
||||
from ai_trade import config as cfg
|
||||
importlib.reload(cfg)
|
||||
from signal_v2 import main as main_mod
|
||||
from ai_trade import main as main_mod
|
||||
importlib.reload(main_mod)
|
||||
with TestClient(main_mod.app) as client:
|
||||
r = client.get("/health")
|
||||
@@ -24,17 +24,17 @@ def test_health_endpoint_returns_status_online(monkeypatch):
|
||||
|
||||
def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog):
|
||||
# Use setenv with empty string + no-op load_dotenv to defeat .env re-read on reload
|
||||
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None)
|
||||
monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
|
||||
monkeypatch.setenv("WEBAI_API_KEY", "")
|
||||
monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local")
|
||||
import importlib
|
||||
from signal_v2 import config as cfg
|
||||
from ai_trade import config as cfg
|
||||
importlib.reload(cfg)
|
||||
# After reload, load_dotenv reference is fresh — re-patch
|
||||
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None)
|
||||
from signal_v2 import main as main_mod
|
||||
monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
|
||||
from ai_trade import main as main_mod
|
||||
importlib.reload(main_mod)
|
||||
with caplog.at_level(logging.WARNING, logger="signal_v2.main"):
|
||||
with caplog.at_level(logging.WARNING, logger="ai_trade.main"):
|
||||
with TestClient(main_mod.app) as client:
|
||||
client.get("/health")
|
||||
assert any("WEBAI_API_KEY" in rec.message for rec in caplog.records)
|
||||
@@ -42,7 +42,7 @@ def test_startup_warns_if_webai_api_key_missing(monkeypatch, caplog):
|
||||
|
||||
def test_startup_warns_if_kis_app_key_missing(monkeypatch, caplog):
|
||||
"""KIS app_key 미설정 시 startup WARNING (KIS 호출 disabled) — V1 패턴."""
|
||||
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None)
|
||||
monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
|
||||
monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local")
|
||||
monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
|
||||
# V1 pattern: kis_env_type=virtual, both virtual keys empty
|
||||
@@ -51,12 +51,12 @@ def test_startup_warns_if_kis_app_key_missing(monkeypatch, caplog):
|
||||
monkeypatch.setenv("KIS_REAL_APP_KEY", "")
|
||||
|
||||
import importlib
|
||||
from signal_v2 import config as cfg
|
||||
from ai_trade import config as cfg
|
||||
importlib.reload(cfg)
|
||||
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None)
|
||||
from signal_v2 import main as main_mod
|
||||
monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
|
||||
from ai_trade import main as main_mod
|
||||
importlib.reload(main_mod)
|
||||
with caplog.at_level(logging.WARNING, logger="signal_v2.main"):
|
||||
with caplog.at_level(logging.WARNING, logger="ai_trade.main"):
|
||||
with TestClient(main_mod.app) as client:
|
||||
client.get("/health")
|
||||
assert any("KIS" in rec.message and "app_key" in rec.message.lower() for rec in caplog.records)
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for minute momentum classifier."""
|
||||
from collections import deque
|
||||
|
||||
from signal_v2.momentum_classifier import (
|
||||
from ai_trade.momentum_classifier import (
|
||||
aggregate_1min_to_5min, classify_minute_momentum,
|
||||
STRONG_UP, WEAK_UP, NEUTRAL, WEAK_DOWN, STRONG_DOWN,
|
||||
)
|
||||
@@ -4,12 +4,12 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from signal_v2.state import PollState
|
||||
from ai_trade.state import PollState
|
||||
|
||||
|
||||
async def test_minute_polling_cycle_updates_state_minute_bars():
|
||||
"""KIS REST mock 의 분봉 데이터가 state.minute_bars[ticker] deque 에 들어간다."""
|
||||
from signal_v2.pull_worker import _run_kis_minute_cycle
|
||||
from ai_trade.pull_worker import _run_kis_minute_cycle
|
||||
|
||||
state = PollState()
|
||||
state.portfolio = {"holdings": [{"ticker": "005930"}, {"ticker": "000660"}]}
|
||||
@@ -45,7 +45,7 @@ async def test_minute_polling_cycle_updates_state_minute_bars():
|
||||
|
||||
def test_websocket_message_updates_state_asking_price():
|
||||
"""WebSocket callback factory → state.asking_price 갱신."""
|
||||
from signal_v2.pull_worker import make_asking_price_callback
|
||||
from ai_trade.pull_worker import make_asking_price_callback
|
||||
|
||||
state = PollState()
|
||||
cb = make_asking_price_callback(state)
|
||||
@@ -58,9 +58,9 @@ def test_websocket_message_updates_state_asking_price():
|
||||
async def test_post_close_cycle_updates_chronos_predictions():
|
||||
"""mock kis + mock chronos → state.chronos_predictions + state.daily_ohlcv 갱신."""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from signal_v2.pull_worker import _run_post_close_cycle
|
||||
from signal_v2.chronos_predictor import ChronosPrediction
|
||||
from signal_v2.state import PollState
|
||||
from ai_trade.pull_worker import _run_post_close_cycle
|
||||
from ai_trade.chronos_predictor import ChronosPrediction
|
||||
from ai_trade.state import PollState
|
||||
|
||||
state = PollState()
|
||||
state.portfolio = {"holdings": [{"ticker": "005930"}]}
|
||||
@@ -100,8 +100,8 @@ async def test_post_close_cycle_updates_chronos_predictions():
|
||||
def test_poll_loop_calls_generate_signals_after_cycle(monkeypatch):
|
||||
"""Phase 4: generate_signals 가 cycle 후 state.signals 를 갱신한다."""
|
||||
from unittest.mock import MagicMock
|
||||
from signal_v2.state import PollState
|
||||
from signal_v2.signal_generator import generate_signals
|
||||
from ai_trade.state import PollState
|
||||
from ai_trade.signal_generator import generate_signals
|
||||
|
||||
state = PollState()
|
||||
state.portfolio = {"holdings": [{
|
||||
@@ -2,7 +2,7 @@
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from signal_v2.rate_limit import SignalDedup
|
||||
from ai_trade.rate_limit import SignalDedup
|
||||
|
||||
KST = ZoneInfo("Asia/Seoul")
|
||||
|
||||
@@ -24,11 +24,11 @@ def test_is_recent_returns_false_after_24h(tmp_dedup_db, monkeypatch):
|
||||
now = datetime.now(KST)
|
||||
fake_now = now - timedelta(hours=25)
|
||||
monkeypatch.setattr(
|
||||
"signal_v2.rate_limit._now_iso", lambda: fake_now.isoformat()
|
||||
"ai_trade.rate_limit._now_iso", lambda: fake_now.isoformat()
|
||||
)
|
||||
dedup.record("005930", "buy", confidence=0.82)
|
||||
# Reset to real now for is_recent check
|
||||
monkeypatch.setattr(
|
||||
"signal_v2.rate_limit._now_iso", lambda: now.isoformat()
|
||||
"ai_trade.rate_limit._now_iso", lambda: now.isoformat()
|
||||
)
|
||||
assert dedup.is_recent("005930", "buy", within_hours=24) is False
|
||||
@@ -3,7 +3,7 @@ from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from signal_v2.scheduler import _next_interval, _is_market_day, KST
|
||||
from ai_trade.scheduler import _next_interval, _is_market_day, KST
|
||||
|
||||
|
||||
def _kst(year, month, day, hour, minute=0):
|
||||
@@ -3,8 +3,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from signal_v2.signal_generator import generate_signals
|
||||
from signal_v2.state import PollState
|
||||
from ai_trade.signal_generator import generate_signals
|
||||
from ai_trade.state import PollState
|
||||
|
||||
|
||||
def _settings(**overrides):
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import pytest
|
||||
import httpx
|
||||
|
||||
from signal_v2.stock_client import StockClient
|
||||
from ai_trade.stock_client import StockClient
|
||||
|
||||
|
||||
BASE_URL = "https://test.stock.local"
|
||||
@@ -59,7 +59,7 @@ async def test_get_portfolio_refetches_after_ttl_expiry(mock_stock_api, monkeypa
|
||||
# Fake clock: starts at 0, jumps past portfolio TTL (180s) between calls
|
||||
fake_time = [0.0]
|
||||
monkeypatch.setattr(
|
||||
"signal_v2.stock_client.time.monotonic", lambda: fake_time[0]
|
||||
"ai_trade.stock_client.time.monotonic", lambda: fake_time[0]
|
||||
)
|
||||
|
||||
client = StockClient(BASE_URL, API_KEY)
|
||||
@@ -137,7 +137,7 @@ async def test_get_portfolio_falls_back_to_stale_on_all_failures(
|
||||
# Patch time.monotonic BEFORE first call so cached timestamp uses fake clock
|
||||
fake_time = [0.0]
|
||||
monkeypatch.setattr(
|
||||
"signal_v2.stock_client.time.monotonic", lambda: fake_time[0]
|
||||
"ai_trade.stock_client.time.monotonic", lambda: fake_time[0]
|
||||
)
|
||||
|
||||
# First call succeeds
|
||||
@@ -158,7 +158,7 @@ async def test_get_portfolio_falls_back_to_stale_on_all_failures(
|
||||
# Now mock to return 500s persistently
|
||||
route1.mock(return_value=httpx.Response(500, text="server error"))
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="signal_v2.stock_client"):
|
||||
with caplog.at_level(logging.WARNING, logger="ai_trade.stock_client"):
|
||||
result = await client.get_portfolio()
|
||||
assert result["holdings"][0]["ticker"] == "005930" # stale data returned
|
||||
assert any(
|
||||
@@ -1,6 +1,6 @@
|
||||
# tests/test_stock_client_ttl.py
|
||||
"""SP-A1 회귀 — _TTL이 NAS 부담 완화를 위한 값으로 설정되어 있어야 함."""
|
||||
from signal_v2.stock_client import _TTL
|
||||
from ai_trade.stock_client import _TTL
|
||||
|
||||
|
||||
def test_portfolio_ttl_is_180s():
|
||||
26
signal_v1/DEPRECATED.md
Normal file
26
signal_v1/DEPRECATED.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# signal_v1 — DEPRECATED
|
||||
|
||||
> **2026-05-19부터 사용 안 함.** 신규 작업 금지. 모든 트레이딩은 `web-ai/ai_trade/` (구 `signal_v2`) 에서 진행.
|
||||
|
||||
## 폐기 사유
|
||||
|
||||
- V2 (`ai_trade`) Phase 4 완료 — Chronos-2 zero-shot 1일 수익률 + 분봉 모멘텀 + 5-state classifier + sell-first 우선순위 + 매수 hard gate가 V1 (LSTM 7-features + Gemini Flash) 보다 정확도·확장성·해석가능성에서 우위
|
||||
- V1 + V2 동시 운영 시 KIS API rate limit 충돌
|
||||
- NAS 인바운드 polling 부담 (web-ai → NAS API) 의 50% 차지
|
||||
|
||||
## 향후 처리
|
||||
|
||||
- 디렉토리를 `legacy/signal_v1/`로 이동 예정 (현재 file lock 풀린 후 처리)
|
||||
- `start.bat` 진입점은 이미 `legacy/start_v1.bat`으로 이동 → 자동 시작 차단됨
|
||||
- DSM Scheduler 등 외부 trigger에 V1 startup 등록되어 있다면 해제 필요 (박재오 확인)
|
||||
|
||||
## 활용 (필요 시)
|
||||
|
||||
- 코드 참고용 (LSTM 모델 구조, Telegram bot 인터페이스, KIS 자동주문 패턴)
|
||||
- 별도 backtest 실행은 가능 (`backtest_runner.py`) — 단 운영 자동 실행 X
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- 신 운영 가이드: `../ai_trade/`
|
||||
- web-ai 통합 가이드: `../CLAUDE.md`
|
||||
- V1 vs V2 진단: `../CHECK_POINT.md`
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
cd /d "%~dp0\.."
|
||||
python -m uvicorn signal_v2.main:app --host 0.0.0.0 --port 8001
|
||||
Reference in New Issue
Block a user