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:
2026-05-19 01:31:47 +09:00
parent bb03cc4525
commit 139e4e3382
49 changed files with 381 additions and 80 deletions

10
.gitignore vendored
View File

@@ -47,11 +47,11 @@ daily_trade_history.json
watchlist.json watchlist.json
bot_ipc.json bot_ipc.json
# Test (top-level only; signal_v2/tests tracked separately) # Test (top-level only; ai_trade/tests tracked separately)
tests/ tests/
tests/* tests/*
!signal_v2/tests/ !ai_trade/tests/
!signal_v2/tests/** !ai_trade/tests/**
# System # System
Thumbs.db Thumbs.db
@@ -63,5 +63,5 @@ KIS_SETUP.md
.claude/ .claude/
# Signal V2 runtime data # Signal V2 runtime data
signal_v2/data/*.db ai_trade/data/*.db
signal_v2/data/*.db-* ai_trade/data/*.db-*

277
CHECK_POINT.md Normal file
View 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).

View File

@@ -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_v1/` | ⚠️ **DEPRECATED 2026-05-19** — 레거시 LSTM 봇. 사용 안 함. 향후 `legacy/signal_v1/`로 이동 예정 (현재 file lock 풀린 후) | `:8000` | **OFF** |
| `signal_v2/` | Confidence Signal Pipeline V2 (Chronos-bolt + 분봉 모멘텀 + KIS WebSocket + 신호 생성) | `:8001` | **Phase 4 완료 (2026-05-17)**, Phase 5 대기 | | `ai_trade/` | 자동매매 메인 (구 `signal_v2` 2026-05-19 rename) — 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` | | `legacy/start_v1.bat` | (deprecated) V1 진입점 — root `start.bat`에서 이동됨. 자동 실행 차단 | — | **OFF** |
| `start.bat` | V1 진입점 | — | `signal_v1/main_server.py` 실행 | | `ai_trade/start.bat` | 자동매매 진입점 | — | `ai_trade/main.py` uvicorn 실행 |
| `signal_v2/start.bat` | V2 진입점 | — | `signal_v2/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 등 | | `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 단독 (운영 기본) ### V1 (⚠️ DEPRECATED — 운영 X)
```bat 2026-05-19부터 자동 시작 차단. `legacy/start_v1.bat`에 보존 (참고용만).
cd C:\Users\jaeoh\Desktop\workspace\web-ai 별도 backtest 등 1회성 시 필요 시 박재오 직접 `legacy/start_v1.bat` 실행.
.\start.bat
```
기대 로그: `[Bot] Cycle Start ...`, `[AI] 005930: NN epochs ...`, `[Ensemble] tech=... news=... lstm=...`, `Score: 0.xx [HOLD]`
### V2 단독 (smoke/검증) ### ai_trade 단독 (smoke/검증)
```bat ```bat
cd C:\Users\jaeoh\Desktop\workspace\web-ai\signal_v2 cd C:\Users\jaeoh\Desktop\workspace\web-ai\ai_trade
.\start.bat .\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`. 기대 로그: `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 채택 | | 0 | Architecture & contract spec | ✅ Chronos-2 + Qwen3 14B 채택 |
| 1 | stock 백엔드 WebAI API 보강 (NAS) | ✅ 102/102 tests, 운영 배포 | | 1 | stock 백엔드 WebAI API 보강 (NAS) | ✅ 102/102 tests, 운영 배포 |
| 1.5 | V1 → `signal_v1/` rename | ✅ V1 정상 기동 | | 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 | | 3a | KIS REST 분봉 + WebSocket 호가 + NXT 스케줄 | ✅ 33/33 tests |
| 3b | Chronos-bolt-base 추론 + 5분봉 모멘텀 분류기 | ✅ 45/45 tests, 실 KIS+Chronos chain 검증 | | 3b | Chronos-bolt-base 추론 + 5분봉 모멘텀 분류기 | ✅ 45/45 tests, 실 KIS+Chronos chain 검증 |
| 4 | Signal Generator (매수/매도 룰) + pull_worker 통합 + 로깅 | ✅ **2026-05-17 완료, 56/56 tests, push 완료** | | 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) 작업 시 주의 ## 양쪽 디렉토리 (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` - **커밋**: `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/`) - **메모리**: 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`) - **spec amendment 발생 시**: 코드는 `web-ai` 에 commit, spec 갱신은 `web-ui/docs/superpowers/specs/` 에 commit (Phase 4 spread formula 변경 사례 = web-ui commit `534ded5`)

View File

@@ -18,7 +18,7 @@ class Settings:
) )
port: int = field(default_factory=lambda: int(os.getenv("SIGNAL_V2_PORT", "8001"))) port: int = field(default_factory=lambda: int(os.getenv("SIGNAL_V2_PORT", "8001")))
db_path: Path = field( 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 — V1 호환 패턴 (KIS_ENV_TYPE virtual/real)
kis_env_type: str = field(default_factory=lambda: os.getenv("KIS_ENV_TYPE", "virtual").lower()) kis_env_type: str = field(default_factory=lambda: os.getenv("KIS_ENV_TYPE", "virtual").lower())

View File

@@ -6,14 +6,14 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from signal_v2 import state as state_mod from ai_trade import state as state_mod
from signal_v2.chronos_predictor import ChronosPredictor from ai_trade.chronos_predictor import ChronosPredictor
from signal_v2.config import get_settings from ai_trade.config import get_settings
from signal_v2.kis_client import KISClient from ai_trade.kis_client import KISClient
from signal_v2.kis_websocket import KISWebSocket from ai_trade.kis_websocket import KISWebSocket
from signal_v2.pull_worker import poll_loop, make_asking_price_callback from ai_trade.pull_worker import poll_loop, make_asking_price_callback
from signal_v2.rate_limit import SignalDedup from ai_trade.rate_limit import SignalDedup
from signal_v2.stock_client import StockClient from ai_trade.stock_client import StockClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -5,12 +5,12 @@ import logging
from collections import deque from collections import deque
from datetime import datetime from datetime import datetime
from signal_v2.kis_client import KISClient from ai_trade.kis_client import KISClient
from signal_v2.scheduler import ( from ai_trade.scheduler import (
KST, _is_market_day, _is_polling_window, _next_interval, _is_post_close_trigger, KST, _is_market_day, _is_polling_window, _next_interval, _is_post_close_trigger,
) )
from signal_v2.state import PollState from ai_trade.state import PollState
from signal_v2.stock_client import StockClient from ai_trade.stock_client import StockClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ async def poll_loop(
# Phase 4: generate signals # Phase 4: generate signals
if dedup is not None and settings is not None: if dedup is not None and settings is not None:
try: try:
from signal_v2.signal_generator import generate_signals from ai_trade.signal_generator import generate_signals
generate_signals(state, dedup, settings) generate_signals(state, dedup, settings)
except Exception: except Exception:
logger.exception("generate_signals failed") 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: def update_minute_momentum_for_all(state) -> None:
"""매 분봉 cycle 후 호출 — 모든 종목 모멘텀 갱신.""" """매 분봉 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() now_iso = datetime.now(KST).isoformat()
for ticker, bars in state.minute_bars.items(): for ticker, bars in state.minute_bars.items():
state.minute_momentum[ticker] = classify_minute_momentum(bars) state.minute_momentum[ticker] = classify_minute_momentum(bars)

3
ai_trade/start.bat Normal file
View File

@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0\.."
python -m uvicorn ai_trade.main:app --host 0.0.0.0 --port 8001

Binary file not shown.

View File

@@ -1,4 +1,4 @@
"""Pytest fixtures for signal_v2 tests.""" """Pytest fixtures for ai_trade tests."""
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -8,7 +8,7 @@ import respx
@pytest.fixture @pytest.fixture
def tmp_dedup_db(tmp_path) -> Path: def tmp_dedup_db(tmp_path) -> Path:
"""SQLite 단위 테스트용 임시 DB path.""" """SQLite 단위 테스트용 임시 DB path."""
return tmp_path / "test_signal_v2.db" return tmp_path / "test_ai_trade.db"
@pytest.fixture @pytest.fixture

View File

@@ -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 quantiles = _mk_quantiles_tensor(101.5, 102.0, 102.5) # narrow around 102
mock_pipeline.predict_quantiles.return_value = (quantiles, None) 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") predictor = ChronosPredictor(model_name="mock-model")
daily = {"005930": _daily_ohlcv([100] * 60)} daily = {"005930": _daily_ohlcv([100] * 60)}
result = predictor.predict_batch(daily) 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) quantiles = _mk_quantiles_tensor(101.99, 102.0, 102.01)
mock_pipeline.predict_quantiles.return_value = (quantiles, None) 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") predictor = ChronosPredictor(model_name="mock-model")
daily = {"005930": _daily_ohlcv([100] * 60)} daily = {"005930": _daily_ohlcv([100] * 60)}
result = predictor.predict_batch(daily) 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) quantiles = _mk_quantiles_tensor(70.0, 100.0, 130.0)
mock_pipeline.predict_quantiles.return_value = (quantiles, None) 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") predictor = ChronosPredictor(model_name="mock-model")
daily = {"005930": _daily_ohlcv([100] * 60)} daily = {"005930": _daily_ohlcv([100] * 60)}
result = predictor.predict_batch(daily) 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) quantiles = _mk_quantiles_tensor(109.0, 110.0, 111.0)
mock_pipeline.predict_quantiles.return_value = (quantiles, None) 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") predictor = ChronosPredictor(model_name="mock-model")
# last close = 100 # last close = 100
daily = {"005930": _daily_ohlcv(list(range(41, 101)))} # last = 100 daily = {"005930": _daily_ohlcv(list(range(41, 101)))} # last = 100

View File

@@ -6,7 +6,7 @@ import httpx
import pytest import pytest
import respx import respx
from signal_v2.kis_client import KISClient from ai_trade.kis_client import KISClient
@pytest.fixture @pytest.fixture

View File

@@ -7,7 +7,7 @@ import httpx
import pytest import pytest
import respx import respx
from signal_v2.kis_websocket import KISWebSocket from ai_trade.kis_websocket import KISWebSocket
BASE_REST = "https://openapivts.koreainvestment.com:29443" BASE_REST = "https://openapivts.koreainvestment.com:29443"

View File

@@ -10,9 +10,9 @@ def test_health_endpoint_returns_status_online(monkeypatch):
monkeypatch.setenv("WEBAI_API_KEY", "test-secret") monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
# Reload modules so they pick up the new env # Reload modules so they pick up the new env
import importlib import importlib
from signal_v2 import config as cfg from ai_trade import config as cfg
importlib.reload(cfg) importlib.reload(cfg)
from signal_v2 import main as main_mod from ai_trade import main as main_mod
importlib.reload(main_mod) importlib.reload(main_mod)
with TestClient(main_mod.app) as client: with TestClient(main_mod.app) as client:
r = client.get("/health") 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): 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 # 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("WEBAI_API_KEY", "")
monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local") monkeypatch.setenv("STOCK_API_URL", "https://test.stock.local")
import importlib import importlib
from signal_v2 import config as cfg from ai_trade import config as cfg
importlib.reload(cfg) importlib.reload(cfg)
# After reload, load_dotenv reference is fresh — re-patch # After reload, load_dotenv reference is fresh — re-patch
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
from signal_v2 import main as main_mod from ai_trade import main as main_mod
importlib.reload(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: with TestClient(main_mod.app) as client:
client.get("/health") client.get("/health")
assert any("WEBAI_API_KEY" in rec.message for rec in caplog.records) 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): def test_startup_warns_if_kis_app_key_missing(monkeypatch, caplog):
"""KIS app_key 미설정 시 startup WARNING (KIS 호출 disabled) — V1 패턴.""" """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("STOCK_API_URL", "https://test.stock.local")
monkeypatch.setenv("WEBAI_API_KEY", "test-secret") monkeypatch.setenv("WEBAI_API_KEY", "test-secret")
# V1 pattern: kis_env_type=virtual, both virtual keys empty # 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", "") monkeypatch.setenv("KIS_REAL_APP_KEY", "")
import importlib import importlib
from signal_v2 import config as cfg from ai_trade import config as cfg
importlib.reload(cfg) importlib.reload(cfg)
monkeypatch.setattr("signal_v2.config.load_dotenv", lambda *a, **k: None) monkeypatch.setattr("ai_trade.config.load_dotenv", lambda *a, **k: None)
from signal_v2 import main as main_mod from ai_trade import main as main_mod
importlib.reload(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: with TestClient(main_mod.app) as client:
client.get("/health") client.get("/health")
assert any("KIS" in rec.message and "app_key" in rec.message.lower() for rec in caplog.records) assert any("KIS" in rec.message and "app_key" in rec.message.lower() for rec in caplog.records)

View File

@@ -1,7 +1,7 @@
"""Tests for minute momentum classifier.""" """Tests for minute momentum classifier."""
from collections import deque from collections import deque
from signal_v2.momentum_classifier import ( from ai_trade.momentum_classifier import (
aggregate_1min_to_5min, classify_minute_momentum, aggregate_1min_to_5min, classify_minute_momentum,
STRONG_UP, WEAK_UP, NEUTRAL, WEAK_DOWN, STRONG_DOWN, STRONG_UP, WEAK_UP, NEUTRAL, WEAK_DOWN, STRONG_DOWN,
) )

View File

@@ -4,12 +4,12 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from signal_v2.state import PollState from ai_trade.state import PollState
async def test_minute_polling_cycle_updates_state_minute_bars(): async def test_minute_polling_cycle_updates_state_minute_bars():
"""KIS REST mock 의 분봉 데이터가 state.minute_bars[ticker] deque 에 들어간다.""" """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 = PollState()
state.portfolio = {"holdings": [{"ticker": "005930"}, {"ticker": "000660"}]} 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(): def test_websocket_message_updates_state_asking_price():
"""WebSocket callback factory → 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() state = PollState()
cb = make_asking_price_callback(state) 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(): async def test_post_close_cycle_updates_chronos_predictions():
"""mock kis + mock chronos → state.chronos_predictions + state.daily_ohlcv 갱신.""" """mock kis + mock chronos → state.chronos_predictions + state.daily_ohlcv 갱신."""
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from signal_v2.pull_worker import _run_post_close_cycle from ai_trade.pull_worker import _run_post_close_cycle
from signal_v2.chronos_predictor import ChronosPrediction from ai_trade.chronos_predictor import ChronosPrediction
from signal_v2.state import PollState from ai_trade.state import PollState
state = PollState() state = PollState()
state.portfolio = {"holdings": [{"ticker": "005930"}]} 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): def test_poll_loop_calls_generate_signals_after_cycle(monkeypatch):
"""Phase 4: generate_signals 가 cycle 후 state.signals 를 갱신한다.""" """Phase 4: generate_signals 가 cycle 후 state.signals 를 갱신한다."""
from unittest.mock import MagicMock from unittest.mock import MagicMock
from signal_v2.state import PollState from ai_trade.state import PollState
from signal_v2.signal_generator import generate_signals from ai_trade.signal_generator import generate_signals
state = PollState() state = PollState()
state.portfolio = {"holdings": [{ state.portfolio = {"holdings": [{

View File

@@ -2,7 +2,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from signal_v2.rate_limit import SignalDedup from ai_trade.rate_limit import SignalDedup
KST = ZoneInfo("Asia/Seoul") KST = ZoneInfo("Asia/Seoul")
@@ -24,11 +24,11 @@ def test_is_recent_returns_false_after_24h(tmp_dedup_db, monkeypatch):
now = datetime.now(KST) now = datetime.now(KST)
fake_now = now - timedelta(hours=25) fake_now = now - timedelta(hours=25)
monkeypatch.setattr( 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) dedup.record("005930", "buy", confidence=0.82)
# Reset to real now for is_recent check # Reset to real now for is_recent check
monkeypatch.setattr( 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 assert dedup.is_recent("005930", "buy", within_hours=24) is False

View File

@@ -3,7 +3,7 @@ from datetime import datetime
import pytest 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): def _kst(year, month, day, hour, minute=0):

View File

@@ -3,8 +3,8 @@ from unittest.mock import MagicMock
import pytest import pytest
from signal_v2.signal_generator import generate_signals from ai_trade.signal_generator import generate_signals
from signal_v2.state import PollState from ai_trade.state import PollState
def _settings(**overrides): def _settings(**overrides):

View File

@@ -4,7 +4,7 @@ import logging
import pytest import pytest
import httpx import httpx
from signal_v2.stock_client import StockClient from ai_trade.stock_client import StockClient
BASE_URL = "https://test.stock.local" 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 clock: starts at 0, jumps past portfolio TTL (180s) between calls
fake_time = [0.0] fake_time = [0.0]
monkeypatch.setattr( 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) 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 # Patch time.monotonic BEFORE first call so cached timestamp uses fake clock
fake_time = [0.0] fake_time = [0.0]
monkeypatch.setattr( 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 # 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 # Now mock to return 500s persistently
route1.mock(return_value=httpx.Response(500, text="server error")) 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() result = await client.get_portfolio()
assert result["holdings"][0]["ticker"] == "005930" # stale data returned assert result["holdings"][0]["ticker"] == "005930" # stale data returned
assert any( assert any(

View File

@@ -1,6 +1,6 @@
# tests/test_stock_client_ttl.py # tests/test_stock_client_ttl.py
"""SP-A1 회귀 — _TTL이 NAS 부담 완화를 위한 값으로 설정되어 있어야 함.""" """SP-A1 회귀 — _TTL이 NAS 부담 완화를 위한 값으로 설정되어 있어야 함."""
from signal_v2.stock_client import _TTL from ai_trade.stock_client import _TTL
def test_portfolio_ttl_is_180s(): def test_portfolio_ttl_is_180s():

26
signal_v1/DEPRECATED.md Normal file
View 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`

View File

@@ -1,3 +0,0 @@
@echo off
cd /d "%~dp0\.."
python -m uvicorn signal_v2.main:app --host 0.0.0.0 --port 8001