From 139e4e3382c8d50cceb8a3045a1bdc5392a8d4a2 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 19 May 2026 01:31:47 +0900 Subject: [PATCH] =?UTF-8?q?refactor(web-ai):=20rename=20signal=5Fv2?= =?UTF-8?q?=E2=86=92ai=5Ftrade,=20deprecate=20signal=5Fv1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 박재오 결정 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) --- .gitignore | 10 +- CHECK_POINT.md | 277 ++++++++++++++++++ CLAUDE.md | 32 +- {signal_v2 => ai_trade}/__init__.py | 0 {signal_v2 => ai_trade}/chronos_predictor.py | 0 {signal_v2 => ai_trade}/config.py | 2 +- {signal_v2 => ai_trade}/data/.gitkeep | 0 {signal_v2 => ai_trade}/holidays.json | 0 {signal_v2 => ai_trade}/kis_client.py | 0 {signal_v2 => ai_trade}/kis_websocket.py | 0 {signal_v2 => ai_trade}/main.py | 16 +- .../momentum_classifier.py | 0 {signal_v2 => ai_trade}/pull_worker.py | 12 +- {signal_v2 => ai_trade}/pytest.ini | 0 {signal_v2 => ai_trade}/rate_limit.py | 0 {signal_v2 => ai_trade}/scheduler.py | 0 {signal_v2 => ai_trade}/signal_generator.py | 0 ai_trade/start.bat | 3 + {signal_v2 => ai_trade}/state.py | 0 {signal_v2 => ai_trade}/stock_client.py | 0 {signal_v2 => ai_trade}/tests/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 163 bytes .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 1139 bytes ...nos_predictor.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 9559 bytes ...st_kis_client.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 16883 bytes ...kis_websocket.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 13800 bytes .../test_main.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 7316 bytes ...um_classifier.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 11904 bytes ...t_pull_worker.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 14420 bytes ...st_rate_limit.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 5032 bytes ...est_scheduler.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 13705 bytes ...nal_generator.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 16510 bytes ..._stock_client.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 14694 bytes ...ck_client_ttl.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 2461 bytes {signal_v2 => ai_trade}/tests/conftest.py | 4 +- .../tests/test_chronos_predictor.py | 8 +- .../tests/test_kis_client.py | 2 +- .../tests/test_kis_websocket.py | 2 +- {signal_v2 => ai_trade}/tests/test_main.py | 24 +- .../tests/test_momentum_classifier.py | 2 +- .../tests/test_pull_worker.py | 16 +- .../tests/test_rate_limit.py | 6 +- .../tests/test_scheduler.py | 2 +- .../tests/test_signal_generator.py | 4 +- .../tests/test_stock_client.py | 8 +- .../tests/test_stock_client_ttl.py | 2 +- start.bat => legacy/start_v1.bat | 0 signal_v1/DEPRECATED.md | 26 ++ signal_v2/start.bat | 3 - 49 files changed, 381 insertions(+), 80 deletions(-) create mode 100644 CHECK_POINT.md rename {signal_v2 => ai_trade}/__init__.py (100%) rename {signal_v2 => ai_trade}/chronos_predictor.py (100%) rename {signal_v2 => ai_trade}/config.py (99%) rename {signal_v2 => ai_trade}/data/.gitkeep (100%) rename {signal_v2 => ai_trade}/holidays.json (100%) rename {signal_v2 => ai_trade}/kis_client.py (100%) rename {signal_v2 => ai_trade}/kis_websocket.py (100%) rename {signal_v2 => ai_trade}/main.py (90%) rename {signal_v2 => ai_trade}/momentum_classifier.py (100%) rename {signal_v2 => ai_trade}/pull_worker.py (95%) rename {signal_v2 => ai_trade}/pytest.ini (100%) rename {signal_v2 => ai_trade}/rate_limit.py (100%) rename {signal_v2 => ai_trade}/scheduler.py (100%) rename {signal_v2 => ai_trade}/signal_generator.py (100%) create mode 100644 ai_trade/start.bat rename {signal_v2 => ai_trade}/state.py (100%) rename {signal_v2 => ai_trade}/stock_client.py (100%) rename {signal_v2 => ai_trade}/tests/__init__.py (100%) create mode 100644 ai_trade/tests/__pycache__/__init__.cpython-312.pyc create mode 100644 ai_trade/tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_chronos_predictor.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_kis_client.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_kis_websocket.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_main.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_momentum_classifier.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_pull_worker.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_rate_limit.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_scheduler.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_signal_generator.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_stock_client.cpython-312-pytest-9.0.2.pyc create mode 100644 ai_trade/tests/__pycache__/test_stock_client_ttl.cpython-312-pytest-9.0.2.pyc rename {signal_v2 => ai_trade}/tests/conftest.py (81%) rename {signal_v2 => ai_trade}/tests/test_chronos_predictor.py (93%) rename {signal_v2 => ai_trade}/tests/test_kis_client.py (99%) rename {signal_v2 => ai_trade}/tests/test_kis_websocket.py (98%) rename {signal_v2 => ai_trade}/tests/test_main.py (74%) rename {signal_v2 => ai_trade}/tests/test_momentum_classifier.py (98%) rename {signal_v2 => ai_trade}/tests/test_pull_worker.py (92%) rename {signal_v2 => ai_trade}/tests/test_rate_limit.py (84%) rename {signal_v2 => ai_trade}/tests/test_scheduler.py (97%) rename {signal_v2 => ai_trade}/tests/test_signal_generator.py (98%) rename {signal_v2 => ai_trade}/tests/test_stock_client.py (94%) rename {signal_v2 => ai_trade}/tests/test_stock_client_ttl.py (94%) rename start.bat => legacy/start_v1.bat (100%) create mode 100644 signal_v1/DEPRECATED.md delete mode 100644 signal_v2/start.bat diff --git a/.gitignore b/.gitignore index e1c9e86..a0b2887 100644 --- a/.gitignore +++ b/.gitignore @@ -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-* diff --git a/CHECK_POINT.md b/CHECK_POINT.md new file mode 100644 index 0000000..0ccf26a --- /dev/null +++ b/CHECK_POINT.md @@ -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). diff --git a/CLAUDE.md b/CLAUDE.md index 3ff4b75..075f80b 100644 --- a/CLAUDE.md +++ b/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`) diff --git a/signal_v2/__init__.py b/ai_trade/__init__.py similarity index 100% rename from signal_v2/__init__.py rename to ai_trade/__init__.py diff --git a/signal_v2/chronos_predictor.py b/ai_trade/chronos_predictor.py similarity index 100% rename from signal_v2/chronos_predictor.py rename to ai_trade/chronos_predictor.py diff --git a/signal_v2/config.py b/ai_trade/config.py similarity index 99% rename from signal_v2/config.py rename to ai_trade/config.py index 305b417..fbfc75a 100644 --- a/signal_v2/config.py +++ b/ai_trade/config.py @@ -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()) diff --git a/signal_v2/data/.gitkeep b/ai_trade/data/.gitkeep similarity index 100% rename from signal_v2/data/.gitkeep rename to ai_trade/data/.gitkeep diff --git a/signal_v2/holidays.json b/ai_trade/holidays.json similarity index 100% rename from signal_v2/holidays.json rename to ai_trade/holidays.json diff --git a/signal_v2/kis_client.py b/ai_trade/kis_client.py similarity index 100% rename from signal_v2/kis_client.py rename to ai_trade/kis_client.py diff --git a/signal_v2/kis_websocket.py b/ai_trade/kis_websocket.py similarity index 100% rename from signal_v2/kis_websocket.py rename to ai_trade/kis_websocket.py diff --git a/signal_v2/main.py b/ai_trade/main.py similarity index 90% rename from signal_v2/main.py rename to ai_trade/main.py index 87ab3a8..0d78344 100644 --- a/signal_v2/main.py +++ b/ai_trade/main.py @@ -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__) diff --git a/signal_v2/momentum_classifier.py b/ai_trade/momentum_classifier.py similarity index 100% rename from signal_v2/momentum_classifier.py rename to ai_trade/momentum_classifier.py diff --git a/signal_v2/pull_worker.py b/ai_trade/pull_worker.py similarity index 95% rename from signal_v2/pull_worker.py rename to ai_trade/pull_worker.py index fe7401e..3f94217 100644 --- a/signal_v2/pull_worker.py +++ b/ai_trade/pull_worker.py @@ -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) diff --git a/signal_v2/pytest.ini b/ai_trade/pytest.ini similarity index 100% rename from signal_v2/pytest.ini rename to ai_trade/pytest.ini diff --git a/signal_v2/rate_limit.py b/ai_trade/rate_limit.py similarity index 100% rename from signal_v2/rate_limit.py rename to ai_trade/rate_limit.py diff --git a/signal_v2/scheduler.py b/ai_trade/scheduler.py similarity index 100% rename from signal_v2/scheduler.py rename to ai_trade/scheduler.py diff --git a/signal_v2/signal_generator.py b/ai_trade/signal_generator.py similarity index 100% rename from signal_v2/signal_generator.py rename to ai_trade/signal_generator.py diff --git a/ai_trade/start.bat b/ai_trade/start.bat new file mode 100644 index 0000000..4c05ff8 --- /dev/null +++ b/ai_trade/start.bat @@ -0,0 +1,3 @@ +@echo off +cd /d "%~dp0\.." +python -m uvicorn ai_trade.main:app --host 0.0.0.0 --port 8001 diff --git a/signal_v2/state.py b/ai_trade/state.py similarity index 100% rename from signal_v2/state.py rename to ai_trade/state.py diff --git a/signal_v2/stock_client.py b/ai_trade/stock_client.py similarity index 100% rename from signal_v2/stock_client.py rename to ai_trade/stock_client.py diff --git a/signal_v2/tests/__init__.py b/ai_trade/tests/__init__.py similarity index 100% rename from signal_v2/tests/__init__.py rename to ai_trade/tests/__init__.py diff --git a/ai_trade/tests/__pycache__/__init__.cpython-312.pyc b/ai_trade/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51a92bd0f05fefaf6b75b4623efabbc612c49ed7 GIT binary patch literal 163 zcmX@j%ge<81nzd+Ss?l`h(HIQS%4zb87dhx8U0o=6fpsLpFwJVIXPR!gche36~|;H zrsikFxTF?mm*f}3l;;;^7Z)TZr^b}0Cg~<-#w2FOmlP$Yq{ft_7MB#q#K&jmWtPOp k>lIYq;;_lhPbtkwwJTx;8p#O6#URETNWZ!Gbhc#F8A6mcmocM{*K@d^8;+A|gBSd?K9)ZOKv6N|=>Q zC#f>s*#O-+tq4s_N>;QSc~*C(Q~$|~m7$r_T9UN*eD`R{r`%U3?K!`|C|4(4rkb|l zGt;7~2yy+ZfW4rRATnzDQ$dVTOr6bPn@y3>5^YvfsFvX}wlUuVFbbE< zoVxjNRpq{$n^8}Wo>B8?=xWyF)F?1Vt*kBVjC%9&&CR7p;k4K8>GNGvzVGqD-rjH+ zA%yO@Inz0h_HP_K_Hy8@GPZGP;(gnm#pF*Jk{8K4W$d*w_J?<1 z{)CsR-G~f0aUdU{{)`9KL z>Wmf{vM^AZt{~p^9qTRE;10Gcwr1sFU RoveqyNix|R`a>%$_ZO#XGbaE5 literal 0 HcmV?d00001 diff --git a/ai_trade/tests/__pycache__/test_chronos_predictor.cpython-312-pytest-9.0.2.pyc b/ai_trade/tests/__pycache__/test_chronos_predictor.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6f58e5522748f65c1ecdc1ba6b5df8bfeed5fd6 GIT binary patch literal 9559 zcmeG?TWlOhax*)#?sW7a^vx*4e1rw-@|eiVUjg!c zMq>IIiIrSAw%mem^a{a)JIJr403lO#?h>o98Z&B1 z+iHfV$Xf-C{X^IHS&%A+pW7~@o@J)E9ZVaeu#>P4=U^6C^D=jinPmTrn`Gns^z*m? z;Y(>nww9eO%DHS_P7IO4m{=sTO43l21i;2gKARsF(o_^e(WDd#gQIdrO-ygg7?Mj= zVW-zssXP^T6I_Ds6UBUbOcq7LVKKU9Y&~QQE>5I#<1)ye%j6W@m(43`I-ijhT&^H| z_QG@f2hJ)oQ3giSa$#iPh^$;x3&nxS0=cLZ(;0bSQXXtgX9v<*5mqJ111fGhg`zok zT(Pn3iQ<$V#yuCUg_1Cgmi?#gh(OvZJgfnf$fCrWGy43P#VX&U-T+#mCAxHR)4@sj&> z&$nFH8hIwem0TtEH1uRme3FTKPEGsTlkHut$yDn-oQ%#T+okN!VIFah?vv80tY*h# zoi7ySyv~nghevcSSD4h@nOs4Ubn_u@SJkYl#* zoIO07e(Sk8ZnnF;rFH(&yHEY-@bad{+0GS?4ab*L$3LZg9x|-^1p6N=3^Gt~Bd>35 z_2~XfZAsAkY(@q&RYAK=G=p=rE;LVcFHQjhMjs76Q{psk)JbJhYYdn!*DyEC4zoak zBd)nBihG&?bJY74Iaid)eqnGto0A0dWfU)sr}JtyCo85E5>z>_fMr%j(nVP~-_|0) zx8p*Bpd~m3GIvYrR1xLvrh2J+j1IbYC|5|U9qnz*^U^C3!s29gB zR)!W07Ni*nac7h`fEnf^KDHc;T|fBh!JFN)+4(Kytq00m4wg6fynFUcPd} z=`w$s#DF@3)3Yvoov0xH4lKJXxB+@$&-T$#>Z~Lqlq~w=77@=E@aPxEQs z38v)N{K{)8zU=fGt+lYqQwnMRAyx`wk0IA>bNd~A@a#k|ool-!GNi|v^%hFO%eZrcBW85aIuJY7^ z*0|R56;|S3W=lSh9>@|8fKo_BC8U=Y(s;{Sm%>{32CHyVtyH&RIc#yvr-~d@!5qlm zb>i$%MIP&WfL!XN`VG&>Deo#X(n6MG9=kI(CCs2+YWS7SfZy;8Y)Y7cb6r*BriCr8 z(x%sWsx^@fYfZ%JYdukGKp#S*XsOm*CBM{IifVo>It`wBDW;+!qn8%5`j={?&81qc zMytID_M%j$)q!R;IdrAkySqiJgLii;_Nu(Q(zXrD-Kkksc3$0S&C{CYV|8&|57>uo z|MR-&kYhDX@LTJ93|(}CbTu2KtF`E)?XP>N2G&bYNKIP3X~h~gtd9*AC+lfiF+W(c zDBLl)Yv8Uc)lTv8C&y*jS@a339N$0v-ERx%UY}R9nTs-MSrcq!3;7EPA(vKEk@`tJ zZOLSd06qyI?E-cl1MfVY|LLn&_qOj!3{w?)>0IB(6qz~KXVlD|>pMkV0SFahR$OEG z^?J?f8W{7l08Y1+hIzMG9ZC@I(TGI%B$KJ`j^sTFM!0tn05}(}Y(CCT*Y0XAPPN4q zA)6Nv?T9P7hser!PQ?fX+7NV1N74$!Br2sz0S#29rc;i}&bSh|j2r+FdMI5ot~8tM zTWCdlT!{xtdLXMpzFhz3U;d}*?;iU2^IMk zAy^uF5N7^0WWCQC6%fa$bXZu5F(W|CHdtuFkbR&ev|5Xa!=!|RwA2O^I+q?);vTXe zCPHUJx)6KToYy{>^Hf{gbkwoMUKw#2$njqJr?P!!Z> z*25NyL5$1wKwohRBRw%q^cpdf&gDcR7fGftR!kG{c||2XBx8*p0!k5GPZh+hdvXOJ zR`kuHGEx}NNwk+ZoGT2bP3?#rrh5x2bd-<+(d$G=AdIC|QGU6YOXt%nw2*zce2*bK z(g%h3CK^f7KnXl<%eSqpLid{s0G4hV5s*$~62$2l!b>^}?MA^EyVF_dBHe2`b~u|; zPA+CggMXK^_(%o=|K{OQGIeT{Q=zMB1);)84xwGdTwcK%J=HwE;V}ajsmrs7opZYK+ z%zgDPZ4WnmSDQUOdvxwgZ#}=*_-o}*a^~oAc+*mN=Rz15Er$2b99s_7mH8*G?z=Hj z4nDEO@|Sz(lHxPbkP(qaX*oA1B? zs@-g%cX3#w76@*hu~8V(#%IqO=o$ze*2h#{&=DF_{R>3wzsi`ZN<76wJcWq~dR2|5 z+!{Y>M_d*q!)R{n9EFI>V@3g9%{v;Y&{$Z>CxswZ@M->wM=%c20?LfTkErIig|z_q zZ4vBK@!K4Is`)5()bkiRJ2ra^o$d2L1jQLK6OEvPrtcA|c##fHRbsqKd|=7U^gX=b zZ}{L2l>Fdx)Q%qoi9Ps}Z#;No2K);}CE3AoG`>RfrS4=a1cLjpJzLR(_^`rrIp^T5~7^^rZma`;kDVCe_oSQ!4qE!Hs7triEz^BhLX+usUI0L|W6r{*GyT&m3&pHp3ZUq^yGWUk1e}-CmoRx^FGI4T5M!R0wDR8&o z2%RKj&>x3a-U9#$)&NtpYxXzi&dwiOj2)M5N@Lx9chCMFxyUW3COF>~FC;&d& zK`VsWj)mavB_6-;BL!3#rA33Dw%nmDMm^vbiBVy|5BS~xw@efUm;Y~~m`9kX9iY(n zuY6~niNdhPHb6-;sF``ujAa-rsOfvjG(uz$7?D;4)l5(ay%rCVUZB{R2{Jr*FhN8@ zu9#(~wuWFhMX~@M^;++&yw;HWuJTq-W+j;vfWvdh#raO3nM=*@T8!=cU+_-bsobsL zeEfI19hHQxYA0@`fE2Wnh}NNLJt4`9PDu^y#hUApPDvFPqvDj%lv6;3-&>4J3s@tS zf>O8?(t=uurko&|seDp7^sA+F!DR`9;}XR_6~{%2soN~>quVX4@^mDDw>bD$Nye(S z<73Sc5>Ax_vryq_Ve5N9!YTH}eJZKu$Iw+=sV|&^X1h|4;b*6sA46xSQKcF)Efm2N zvlcO3v#5%8ie3&*JFU9PQ?nCRmOR(kJxmV;;10qKx&Urk{rGP~fBH+4@Zjdv2k*Q^ zy{}5XN4S4&=Kc?_Sikbz|BIP_eeLEW5<=k9q2)BfMe4lmZA)4?p}zB``JAzkch7%; z`Mej0dELXjmFdC#@DS;Nl4V0_#-!WIiU6`UV^SrD11X!e856Lu&Ws6A82JD*V{!mT z#2gdBnV<{UO;aQ7t1~7r7On0yGbWF81$90L4wmjF>HM%v{X+8V$h{9i#W#E&izg6# z6~Ss(F&^81tLStP+sJPrmr(@g0XW`kpXO{ITtg3(6OqNQer@`%9(_M;ZJta zgU9?^0A_N=Rap|v{|&$|nUF@vLHcDv7S)1SQ7}>hj_Beln;}BnKo((@bNP>$MflSr zZ#T@5`HtI7i#zu%Hg=ao`>Ui9q764j7NSp<`L1&G$))J-h3Ia;=jv(2?z#GfXe*!? zY8$zLuK5OR3MS?o7J^+%JbvFdD~ys27qFjETCAXVjge@_JBWk(icA2XtG7{_2{5bY z8;t5T5ExOfHQuOuMNukbL{axyIWdCDqHht6gj&ML2tk~cAo3*1s*p^xt8}kfM^#M^ zkLR;0Zdw9!t@M8$#(zKD@as7I)xwo8>K=nF{7RdB8InMafM@Cv4@B~++^3g*oavV*rx#(dxCu!;Mg)Y?7nslvi^|-7 zhNh^q6i*$dc$&8)>BAOsWro6>r=mA^lJQpyUkV9qlb7sFC| zFqR4?#B}Ntp9JsM4w>yFvpsS&+S%2etLh5%^|g0)wRd;=ySlgc^uZah$nHoq8WYtD zl8$Z%R16f==%?8tEJXnQ1nBoYrxLWGYU!B&fe zhVg}>WL$!o5xJ~CG8hY==v3ZOD2E5Q;U_)-TSh+=xa@WkVzS~foS~95EbTX)wz`Oy&I!5)= zJpCGn!wLgoIS|D%+JSSR3VyfoGkA+WOG*03<9CN+I!;}*zGGDiKn1n~IX4-cNG8)h z7#qoX^^_Qk3Nb0?>Io@Fd#LkJz$)88G7OTk6B$JaGNaLSHYLeU=%*8LLCQvwa+%ga zWJm;@W&xUXmWa(dC3u0#*6ND0t=zdt*&I%x0%R^g-;hTMq!{+&SNzHYEAM`i&iJwfVQGAi&{QLn=9_V=J8_fs^Ci@Bq~!$P*KbC z(4lI6&k`;Iu9}_^P}e=6E%h>9@S2 zE>65p@@ayJ8jUlqTFXyRV!f&UIzvL^B#*{vZ4}HU8nnB{_3jcWn)mTFD{hhR{w*?k zOE628Xo(>SEvPZoT1wWcz27o&HcgJNJ$Jgh%I89OxsY&Y#Q%xllcHW`Y zq+MU3`O1^@s1<&8m<24~fL|luh;-D+uNrmbop~3T8P>d2Oq%L1Ju|E@Gn%lBF*8hM zO7((MYs)fnE=`VaK5rrK?dDr}f8MS30=ME`;Ix{wtJ(`r?1@nqzk1ZE^@Q^NC#cbt zy#1OyzWv(Y?ze|kWwkb&jH~wb>T8NcSyO0gq!Qs}6xPLIY8y3SIo-33I$gFFSEuFb zft5p9o3>F_SfhujlgyW>VfwqwFdb;k!fHg_oOwrH0hF_)Ct#O7q7)qr9~IM?Fdva(YC4*d2ExbESwVJ?`{|4jRoP<$87((Ctg{&b zSdJ7skxqV+I3`IMaeGHcIulDpGVv3V7#vIsu}D010-(zfpvYh}J+!^6Z_AeMjx0!Y z@aZAA6OXowQaU==aiX*1cs4CXq+9Wd5V^jJiYGD18W``|t7 z0(&|uWwKJ&1zJEI2w>C=5Fsn1!Y3lhY)rO=LOp%mp#vA_Pb?2Uc!AD^)@{g)bO%J| ziz?%P(2rawAa1~GXFv=%NBs{fxmyFG%p_wenLQd2#0#v z!ck+pRZdklj_q75qgnzB{xwsbQwOKkPghQ+7V7=e!wRWux$OcRx#$7W#R|$=wMaq9 zkE)Y;f;2-@ZkLvb#Q}EOy&1 zj`H~D%2uEH+@i&1ZCda(&ighOe4D4AoAYg*_ii0yZ+I&ww!Aqq%Qa6v3S?|#^3m~^ z=DB7#uPaF@}=P$pwMQM|E+Xfw~sFRTDeMhbM*$ z-j-Qz!&DEDS#Qf!Pru0&v$wO1C&pivwSZ?y! zS#Rq+hv#)Att93!p|Dq>z_SW#rXcaYr}|8CJo#p7o?BJmR^9oR4j7olR;qm4okb_* zXu31juiwAQt@?NIOE48)@$9doCu;VE*gxHA1@`jh>V4a(cPV;b5Bn~?rv?%~s;z<> zKiWv|>t%nm$%%XqjrqMa^4qKT`VOCO=@g zs3WNr+X_0S0=@2DYAXyN=3(VSOYZJFHpw7e!N#gbC(T9g`?wKNymL!M)4K>KL3aS5c*%uiz{5_PBzrISDOK za+qpbrR5pdd?{4v21IEC3#)3>k#~$cRUEloPl9CyJ?X;|Mo*faUh187WDi!yr_|%) z`g~fh#|=redcaCi@cF0RlMa}XOadJYMTa(v^jU16Q#W8z`g9yHDMv8ra;5KwN$awx zxVCil5ikncI-LRl7mpy~y3%Bc+uxG#)jwFcOxI z#ZqB0OB{KIAIGTSacG2yU+iy)lAiqXd{alEsbhw|-qdwRgl?iJ^U>^#*fs}xUP>~SFTME=9wFJs# zyBGtIEeXPYEc*nKCy^XL@)VNKBl!Z7r;!{)atO&6kvxOsStQRPA@I$mz&F=WItA`H z@Onm%2?@OWWh6sLjsP(uu1+)JS_0X&3K1-TO&1Z`8#wHd`@*o&BQBhTDzK5_^Uzt! z5QA5=%6Dr24Yq56_0F@k1-5q5JKb=N-SSJ;aW}5w%FcMkJrm;8s%u>EhMo#cKRwlQ zjq6;rGIh37Jh)Hob~DVf0?evyny(EOYJ<}!=W2WBD|*Ll3-yh&T;te@$;$EkJl6>4 zbtS1JbXve%B{gTT*SN;XN?k^0KzPAhKFhU?9iP|_NNJwK^SY8&5_6bPSe-(FiT(O* zjn!`h1Eq?9&Z2{=?7IVQ;t~)CXV0rUx9kC6(_6h8fQ_5p4Zx=CaW5pUtg3<=S32n3 zz3i2c75N?-^LuIJw^#4+G4FZkJr&%0UK{d02J)8x8+0=gU{elXWwEm}VTM2;hzCz= zt4u=mGb{NDm2|gd0wGh;fj(*iJXj5QfVUc^5FVqJ5-v)dt3?N|JG5ZIwwqxGL@z7} zbkr9eqimiPN6;B-QiCNxh^7b#oVc$V1W2d08kCzMSqgaQlAzoi%gE|(Royd6LbfER#&MlYcnEII)AIRLbHv4jb<7-dTJtPYv@@=tAC?ON|GJFdDUjB7bx zp^lf6uLK9TQ-y!76_1_{rM2hd=mGrWp!b->`l9ZFq&8ReZ@>U8b^=<)bkMR~Y54B% z=}4>2qMn3^r2nJIOEl zc?(@otD>@V8%++MF3pt$jNwWOqW>huJ4eAG11^l1s>0FF(q90yKj@xCBnW(r9fNTB-uiW(dwp4mID`aH@MpT~LYw>;-OQ{qhE($h07 z*Xnkj;^%CSE?U7qVphc*sA5m`yk}j(vu^6xoTp=+>zL&_7QEGG^5c2X$C@ryG%9N3 ztf6~V7Ig2Js)>=8)M%0R-Sor{KYdbjlVSc(u`%! z+dalEc-v;V;HnYoagYou4X8yIg(O%okE#ua8aN%+}BwBwvt_}q`j0QbO*df z_fXQW>}2=Y=*#x%-5aROjr8ud?B!LD2O)9ASp_$)G|{`;*elIe@U0Cj;jt{2Og)P+%| zgmTa8z7^^Ia)uJtY22zwZZ}k!siqR%iM*~Nv|fWn-35y}Ln1|>n)xdA{i}aR z-`_{xpW|!b{kc4wgLyvckT6nD4kO|S(ZpIcnh20|)SY*WKR5M$sX0=Ycf%a1Um8#2 z8w{14C;_81wMyd5#ylwjp(GuWXrl{pW}`%j8F+UIfDR4!FJt(HmXXtE?J{!u_qdFl z4oLXM^HvfsE#p@~Y_?4O3OpXjkJ$1 znN$9oZ-4E>H~*6O_KwDR*ij}$k{ug>-4qcA1Afc{UspWs|Mg$IcJq~)U~cd;iu%Ii z;;Dgl67FpW_hWlBnj)}V3;RM8b}Iz6nJuA^uohkb_TqU-bHmWcOjr<)r?d>HOUV#6 zgCOz8nIOIF(BCxv94yzwDJQmAb`I%O!_bWoh}*z(h)J7?l;>S5MGWH+ZyHf zv^A9Au`(`7y(SOgGuSwU>xxV8>&P8MVt5UALNZXHm=(mBn8f=LBu9}D(;^Buo2L1Kw`-om^SYX$q;y)qa!P8> zV6TyimF%ndB%Osyb$*&x*dhv5;(I?;@Iouwr>#IB1Uzjmc-!YWJg=)6N@|Xz6#gp8 zE3BD<$bz>9x=-BuV3vKzNG)+_{3@Vjj~j4j{CrsJOS(3f4+ z2SU{47W%*@_OkzpE=c^OvI=heWF37V$o{0wiu@)T^Mf?6FYi4)`C3rRm z9}@5&!CSux)|H+QI{=M*ZJ(;7yK;F4JOfH;I+g5S)m^SMb^3EGwTzS3*uz@FA~a!% zQkVw!u+l5q!^(oq$tH01DLTMfqc2u!HY?bjRZHwntAQ`_y3=&j2K!j;d0XC2Y*27r z!ai1?R&l8vsj!2U1N*{#C{5Y5eR%Hh(zlzu*E=V^ar>5O`SKd7cUl%&ZVNa{k6S_rc zJd1B8`x;uXj4?}0WlD@=UbnrLk<-63-mfC*GJZ9`Mz_%%EAA29uT#_`=&i7UwXOBY zTGAsPX`S|ld5_el@!A)Mj(_p*_*Q=1if5jAW|x?GQmg(9b#+|!TezzP%*1ue^rvpA z@@=15%Z=VyY0K{_0WEJ^rsYN`WWD-rcvn33UTw^^tNJx~U>vn^-|+wAjx%tHcgXne zOU>B3Mse90dsm77y>TZy_tmk7uOHSf@JOs=wF{8dZsTwHYR4u5+InMLwXxUngyB1= z0^b39I;i>%mM_iTC*Zaz_W(xQ_((iC;)hK;nRFcfE2wUR^=}RNBVrV?!IFnyxFJ0f9ZH5_L;7dtnjmjmv?KkV5kuS$#79O%WwVcft`@TAn(Ss4wqx>P zB#$5gHBAYh1M8}LtZu&rB)q@ppIf#D0VCVvCQQ%Mk#?DA2Z2YazuK4Bl;8UP|Q;(hiK z$zI$oP5R8V<=o^tC~^}Nc@n1aDHo9J1QIA${0E2ev2GDGbo&*%0~BvStL_ajwO!xgy;a@e{bx`f%_6ZI3>@MKfPFDF zb*B#8D65?4`ok}c(eHc8CpyRb$Cw4KW}a&*a7_#T)@f#X=LPGG<=ZdL?EH3mMx1SZ z;F9>m&L3odc=QK}_g4Slh4&85KJbOv=BKrd;1|W2o=cs7-uKQ|7wTGWIjk+tf2FJr z*P@;B)SC8!m+(D6T=a$6&L^(bJ$Z`1W;^f?Wi9F+aHr~K*i`Usn%;A%Yu2}E&iBx~ z_n|TNm;ZM*g1@hB1kV+`Yi7B=sYBCizWv;EqOh(HQnQe^uCL(jo9FPnuH`7nIg(WP ztH7^Q#grUfW}%^Z^7y&+Q*GxkAbdJE*>SOZ&Wn7(i&0|zypDI3)SSUy<2oh{8Ra&V zz1XcwYAi_3F0Bznu0jl2ZNU0M19(b8r*pFn8!zsf^M)W%@P_6&K+`g}Dc=4@4PW3HY)xQ3l;up-|=WBwW%`PS-#4>NPU^uY(Xxjq~64>OP-sFUsC zFrSWw!!oD+ZH0Iv<@&JlN0*?oRKnjjG z4M@;YpsYjOj-fQ4O~xJ)EQ&dzsk&cK?w?U*Kcn3L zL-i44Ld^$mu@3$r@iJ$hA2|5;gI14E&j)TXq~|xO`L`^thCzQ%!~l7 zXH&w+Mu1b6a}d^Ne9@&+IViXacb9WHRjE|?cU7rM?IP)uzJLgZj z-+T%-h$Q?;*;A{Ye(!bnd(-dDyzYLje+UNs3|xD*^S#qShWQK&#^unFhhuE;p;Sc)C2a#oOhDyEE4|D}+d$21=@1d>` z+_`kPzpASWXk1;@La29zF#zjM+{4UOz1UStJi|;^9q|IJCq94;gN~>_Kktwr$x@^{ zD@OMH-obARM-FC_eS#djgNz5F4%OEw4Q7)2p;qhh|MMH4Yj1B$T4Q7w4-`(p-^0ImGR!`P1*OTX zLC|Zc)2M*E%o&9bL4hARuIkjiDCv<)_8Aqubjpm9b|((vBwUKU=DMo)bIol?V;LEy zR;Tk2FY#UTUxg9kRbUjRH>}}yXej#>L-;p=_D>xbo$t6@OaZ2=;80k_(bo*zPQ@vG zAXgily)_1A-AO=kcC#diJl&3~`V*6oTyN0yHW-+7H~K%uNcA@P^DPPkQ&{6UMLT7q z(Qe%>$_xwrTcfT-Yt97;UvlK#@T?rEBGn4$n3h~}lOfqAX|Yjf-4&NHKI8rtOZXFP z!3la~dLtI4ffZmT3vR`!xT%gkibwjft&hq&_JEFSkjJLu+GnO?#u)pYIyNam$F;Um zSJJW5kU9sQxsplUC8wr+QcoHbmp&KXr=AP1(W7xU<^tx!Q*aORo0(DeUHQk(<7{-^ zKn!N&8?PNa6xonX49G_}Zs^SA#O$#|I`Zpx{v?vn3-LZ-Ff#M;`I(O|#qw2;lHU0R z2vGxe;(Waf-;wo3)q3Dxe_}8XA0n<-%4YC`Bnt9?n2AGI1A@x-sjPJ9ic|Ff3-+e^ z5s`!>kg5KZ6hD>{<$*-{z!mmO$F^-(*nG{(=GTN9kV?y`jHJ5LS)h>A z`EluJb|6hCX}l+$J(5VrGl_md^~I$`w}2AVP$yMRDx29Yidhl6&x-vCIWC;Yr4yNi z3@vJK)@Y(W5m&ibhr{KuYfZr$?S)pYpb2ZY7dq@@dW$BY%}%yO!i^weTwlJr>-odK z4@!~__a=nw(ZeqZQlFg79X_5F`=neVDI7j799f@89Zsa;vX~&kVN{qzVZ09(4>U{K zMwuH_*HCRfu4!0ei8y@=;zEYxvZ;(LZiaDG!Jo7RwyobVza3(}a5FCd>AffSo{x?$ zEG=4B3a=mMVpCjOnQOaM-+1wj_um-Xb@`7hEc#0kHW_F5(6dzluT1a zVS9Ol)4CJ$(+Vbo&u%iXz}*|LC=IOOAl{>}4Jr=BNjEx9;b5aPjgeklTaR4CuW;~8 zJo4B!x`2#Yq*pJ#eUDs*Zm2O~$**8`SSA8&bQa|k%?{Y;0&~!s`k#Z&+z?67+~`6i zY;JUJd7<%WHc5+&I_s{OTc&NJ^T8(PfIqI_l$RK^XF>)=!9%JFUd5w$P1Ei_Y`s*T z4K(elk;gV0HBUVo3vBW$%!X{5Q^puYJ4c0Cd7J}grv?m>XS6;nZOyelEJ|t!J$Ws8 zjOVlIqr%fE<~s(45JwFmA2o!0x*_C$>h<9_dNl4}2;uthz@M|=m6sapqj#BsS@$y` zG7q=_42JAxQu~w3er;;`TM*nlraRwb( zq?__V(nJ<3L0xOUr`DR!kYwC-tp!kX@Web>LbVW~aH+0~Wnb%pEZ246{VM8W&u7-{yVrU9Zzys^kL@ET9;<|E1Ic}+o_0- zyog)P9aI^JY|&jN%>x48$?|}jv8gpGMJ-_+=+~r$jJO@SInxDd73V<(Izt_KyQ{TD z^MkgCs9)86Q~;Y?R6Vj7mj`p;?X_Of{Ws{waIo5=(iMleTbI{9%ddj|#h0;#JCY@X zswPMv_{0mqH;KI)Uy)N%~hT8= z0C68ij3QWrU@d}m2-YKb2?2U2sw|5}uioyw%v(CNbAqdz4%Q9r9DaG)A9!!wnRTNl z#{H2gE>hwm)ZtiE<`S4WPs~ja8TVWm6pfCiM!fWWqAH>|JoBkYe0I&E}ij zvXO$>Y@icVmR1cEW7Q_TiGZ+M{?L1C&a4^zgR$0f{mOCwD)8}Oh$qf$D=sMW%TY5D z)ywiRX|`UVLVGZHsuI84dT_nkd}=^|1f?E3^TO!yGQVWZQ|6n&zq`DbI(f~P_s;h5 zpu%+LN_?~F!x`14{{{$ne6vJ*!7nXwn=Zc&pu|Tnzh34yO>y{})GM^oI4x=TO{CXQ zJDlJ)JqR+crJ8qms3!%Y$EN*f-R7GyO!1-17GRTME-wg1Ek)@RdT^mbdjG z^O1vwL(L$!)t6#=PY@Ulfc@Z9xB<4{Qe4ts$)+b-$-{)8vkR=s5OP>nB?;T?#R|6Q zEN{`0Um@(OnEo1g0~Tck57eRrZy-Det!1xP>GrDI#!4Lxa&KwA9i03p4dkg)98txton*RKaHvtTx2yozf0lskN{B#S%{)aY+P?Nh%6wT2|)ZrGpj7GSzW z2>!zGS6!$Y3`92!xFA>qc{>p?aXhlRMKR(WN=r+t5{t!F)392zoPrb}ab1L@l5#ZW z_hTRcx$YM~xqE%c7FD}nyfAa_qsXs^&eQh0Kl?crrJRtA%zRXYpxcG}A6&S9;Zo%O zh2eYOy9AY=-23C7%$)s;$jtZudZu_Xa`&f0Gk^IJHqU(je2jK^>lC$uZv=ca4Rjff(}(@;)oP= z(deLv-vn_#0t`Ds9>icPOovTLtssh4Z;Xbu^arkFc2w$PmfqN8;*N~gNJQ$m`+sNwYzdvT-~o)c;Ct`#vFx9dYJgWwGWG@utp z>^lg$0N8_cvl1Mhv$t%~GBvcZwYJVGwMn@9Y5~02P>bempwYa?H35klknWI_r8ur7 zb6H5@Nf3u2Pw7(#fk4heV0Pxg=WZsncIf44&VPFU$^D~kqbrNc-d{7pH5oN4C%E~u z@W8_2u0MO_gIC7BH4Z5c3)h$Yv7ueJgW;**@^Wzb*sk$lGss&2k+;r~j$$1I-ln)t z_)ThMtzg13)@r433!UIPi*+VVX$kV%f4}59r)w9Se0AvMk(Z|Hp{u5mj!}p}L09;j z)XG}Hgk`MNO5+v^k2(r5DV@_qfOwj3C~=ENUMQ|8@r$Q8{7q_QtuT%S4K*P!6jzwd z25L3~0$l$e1vf$rb!118lx!hXEL-8w9cH*wLx927Y@l``2#en4!=pTn3@;yZmbm2@ z7j{BCbc(~@q+X$w#%W2zZ*oAdp>_x))6ZUFxOQ}3ISi5P*jO8YQaCc!Rt~QM^xd{n zc-1(+8qmutu!&!Nc}1Cr93cEn>J?hagk|K@N{}vOpc7QER(%tDGEop};+J7@>C){8 zFo-Y0OFrwA%uJ*CN7w*P`%m*`A`L&ngAoX+G7uVNDxI7W$ays~YayT344yugI^!Uo z9`>w%#1HbkAkSG@p5h=r2nyydjW&))r;A!-3bx+2(M894=6t49*azHE1Q1k5vWFcyg?vzZJWw#JXAt=I=oV577d+z-lkfT!+joGsxiJ#OHxYd;tL1YUqQ(S#Mepax`Ic_Bpe^ zJJBb^X={EP-PuLK7-lvTCn?D&*#~W%He2jy)lt(yJGv`=XR2vKxoN|7cCu;X?NaD!DiFMMMbfBqr z23lyX6CF~Kw-WY<8DxHowZ13D8-I{0F6uf!M;U za#iU8FBq+Unh%@I4rF8rPqy`=`~cOFuEzY1|E-dB0f#`agy*-Kny0-rqpjzH#p<%R z@mz44uOCgofnMuPe$lO(rKM$U<2CK2K>Mxm{Bv)Xyp8%itc|g%*I11Q#M2;6a||lj z=yyPk4An=}t9m3sPRO#TnaK5qh*=BTW)ZJN)^!N*s5%-@`TlGM&ct&GNGulL#s)kY z5ziu^Mz3YoQuEicZOoIt()c}s0-p+B6v)5Y~WRhiNbL)B|}oTS*3dPI=UtxonCwO zwRDBxKqe*Qti>>0fxbf`zU<<10IFLPFJj)L+F+zo$7bbEYuOY_k!KkK%#Ws71k~lA z^L&M&R*8{Q)1M^+X<@tgAyDEQD!m5afs^>?h?`QN6rzmN#dmS+od4{- zOF}ZcLmeMLMJkil(Mm|oq)8*9)%szY=rpNYk=jlDVkc%{w^rTOFRMnSseI^`PkWv_ zpYIZ!rYmfkG{=74=Y8Ji{eSnqzxT}t9Ot9p)7Rxs6+rt0Bj(3ZQEnXqHWBG_Kk&@{G4WV0hq@4xmV4hBSjq>a=@3>N_5jyA+J>cys zI9K&{THY#WmXmj;>P+1VP0voMz~q_KIxEH0MAq~vxbm)4$h2Ew!7X|Vp1eEn8E>bk z0-I-*H`VoKdaB9PtWfmj*-={bW1dmR6&O|ghyitjp;1)o8>VK3S1RfxrL4D=zsd42 zwYdLc*Qy~nD-`WJ?2x}%of;6 zy4SQ>Vcut!WQH%&B6W%`xZ%gbn7dM2EN@dY$Eo1Y`^9=3(n3I72lEohv-%8ic}|gS zHP@aY4rVBXd8`?-<*7A30kf5B_*I`MvEh%bcfnw*fVs;tD?9w>B>%5Rl@)JakG(fqMDrF`$Z!(?B5>-x) zXsnRrRU(KI!7Wa66G_erA_+i*;Lf2rVWZKU2}#xbwvK7OCvwv`db~iibv%r%%o8a` z;@NagAd(h_h5|Sla0}p#&T2OiER8AH$f(j`blTxyoNWJDyXEWo{bWTv?V+7Yz%^4p}Xe>@0 zpU#Rfo%*gw>M_latuh9@!yegbk33>Tj7oReBadp#NLHM-J3nlX?65m`+9SIatUtWs zv((e`*zj{O{mO7kkg{XLPfE(Tn#~PQWy!db6XMeFlr-{?kQ_EAX&8rG(It;vjOM1b zt@<<=4=FEY#9TI+QDKDD36fFx%A6Sw5_t?-flQ#Bhh6zJb%O~mGEF6>>B9Gl?fc72 zkIitID>J)3VH!VT?!Uosi%esQX}oZx%xw6GiCBi?W#*m_y$u(hx)}RGtn6)@J$54y zdimhFgBPY30&QpM8^QXQ3+D<&cJr(g#M#2EvlQIC$l~|95jP@LSpw^KjM##umeJR+ z&SP0s_o@QUSlQ&c$(K%@Wo`taP3K-HvXR*q5NBVQZ7BsKi!6Sx8*wADfDuETM`uH| zOYp$bo+$kT6 zQjm6jv#oDyf2;F%t*-udPuv7cNT59M(>d-C$S>o#GqyDoXbWx78^5i}nqGSwp-dl?=`Jw9gtg0L%pFpv#^CL>s`zf{ zpIVG3`rinced&aK!`k728f)-_SsQRAQe&0t%sPPfIMWB1cEvs=)}28TuEBs$QdPQf ztKT$B%U8(^n8>5NA_m2Jwbe{bg-y*0%~F6RH>jIUd+I(@vqH7Xmu7p3J;(i)r>WhZ z1K6_OR!faMRu9(jv)FQ7g)Lb%QmxtQ$yU>5h50IXnLROSvH;KKTBLCP1A`P+C5X`A z(nfQIjT+lO@PZ)nVivc{xYZCt!}+;|=zkF?WR4!3qvjkMEok((f-h}2O!h#av3q49 zJt7KSGyC)<8kSfwXwiFKnX;;JKy z8x{hVoJ}MoLU6@fc35?S;Su=VdKAO}ysEg+eH)H#aL~k;3al$Xlhlvsi!@E0Z)B)x z`b{U`phF)VJQRDf$yEh?a04hk=OxM?&ATvK49w`Oi`M*;C zI87BpvA0jY6~FNKZ2YI=KOQePbi94?p9(%NFSdMxUfO0OKEODGAVIMRvc1BT5nloD zD>FUkBzU|fc!DJ@Am*Ah51^c&s)XRvp_326MMbALf=3)3;Q;ScUX~^$S@Ynq!}Fzi zp6@<9*gx2(;~l|gnS?=T9vGi25fyS6O}L&AJfabOQt`zn51_!l5`1sxyyQU$fUtxd zlx?(O8>T*lB8p-=iVhUJK`8iuRvE~6Ngn(+MxG2qeq4~1O*+2(3Gt=5#55OMI!n6; z%1q48FOD+vpe=3y&L3+4&F#h9hB!C8!IxXlwO(p|z4f)$w_4A(UiWr?$Th#pz8-ij zQ0BH=ZmMEWInefnld8M_&!O1WWAB{&<;jImtjNaBEAUt>1W_#oV~ecyy^e{7VZl<% zYz#6O=6TSU=K(tyzwl7L{dCdW{V}(t$Zc7IdX4y!f;0E-JxvETIp5tGI#BPr=I)^( z@?O2`zy|Mo>piG%avj*R3TFHY-o^hmW~_oD03GJ`4FFIZX}qFuO&`DAM+}IZjx+(W z?k*rra2e7B#QOg*(yV(UWcK$iAx(q1u>#U?_-B?6@y0tun)PDS{}|FVt$`|4C|kqN zLYn5gf;1dnkO9)Hn1N|-CAzGNG+gCUq-A0dR)B&2BY01YcoF{F&#vA)^OKviuY_;D zbSYe6k#J=%2)Co_XRltp`HM?oF{ub6vLr^re>wfb@Uf2YXXh_}cIM|%vKNZHL%#S{ z?d5(vsQ?G*4PBjs^linW3k!abh6o;OwE%R3?;SkGA3brXSMy9J0l47Wrvc5!`VSv{ zt~&;oKZkg@{2>Q0Lp`P(?zNeb;0EsoO5I6Uk$_f!6nP45{U~lDCu9qqoK#S%huNgM2i2B}&Ifi_adt(D`c5>jSS16dN9xZ-`y)zuNH5#v?m1Z6 zb8ul#thg<<&=3RH3Xtu3lqSzYAqMmuLG37tArQ52CGwS%KI-_`_j^zex%xJGB6XUFhofAa=QY;6lGOQD+rS%_m{9}C;bJjLq$!eA;X<-% zLNL~C1_?h8Q>wX*d|GG}{v`(oxF|jrm77L9#^@JqIR2_v=2PxR0pF=k7YXhyMKDApJCb>%InhjMfiV{{lR8 B4lDow literal 0 HcmV?d00001 diff --git a/ai_trade/tests/__pycache__/test_momentum_classifier.cpython-312-pytest-9.0.2.pyc b/ai_trade/tests/__pycache__/test_momentum_classifier.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d19114126162a493c9d2bc87a62b7c4ec0748d39 GIT binary patch literal 11904 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 IYv|+u1DW*uI{*Lx literal 0 HcmV?d00001 diff --git a/ai_trade/tests/__pycache__/test_pull_worker.cpython-312-pytest-9.0.2.pyc b/ai_trade/tests/__pycache__/test_pull_worker.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b66aeac23aa5b67e94676b30a8b7ed28d24b2947 GIT binary patch literal 14420 zcmeHOeQXp*mhYbF`Rwtx!RFHgwy}o~Gsf5$f-xjGA%XDChULhOhN<>2W;|oMdmwgZ zPTqvukV{TsFS{FbAuH>YlWdV)IZ|};AFB;Xq`RDM{}^K|53P+hTy}3mAQB0=QxFco61GJoYfmRS7(8@ta$Un5|proiuus1CS2QtZI*a3=D3`jtRPKUSSd8+QI{D#uxVOu2Z8YAptk-zAaz4_W^Jjfo|B0g@BW6YAUv2Ma zv3)F*6=5G+iFK@fjI6rr{b_+sf06x3Z7_Oi^ij6gD6;qbX${z~R@qu9QpXyBJIMKr zUfe;>Z^)6=7X-HBku{_x>(TeWz+LZu0Yj5<*Y`g!?0-J^6LWsT-ntdWNLlxL_#&dl|N z@zMC18_&JkBBPB0M&G=MoJmDaB$S9=IW`zeN}Bh0I!O|#xT3k4BF!BRw{L6<<2~Hb z5!PG@RqBW0a9jI^b>X1}>%!|g)`Z*Fw00g0Z`>3P!*3O3660Z>yZ-7ro=fZkF7caI zAREIRH|Ey71iGyz`Xx zTBbUZQYs{%sxXpuNY=}%LFtgaxs9y3jnKw=RCMP@g3*JO#D>YA3sq~?(L_X*qeN=4 zIVUpuQ8L=nnr|R2tG($Y405>=lO-u7$xvN7m5@%$U=MOKx)fS@TvClyn?9b5onkrm z<}}gvN@)JA1A{6!Mv#!#}?P9I=M4LUK z*J16nvnOZTTEhyuRYCjp{HERc`aSvidxv|@?0Mf?cCPdM z;e6$a(c1COeDmgfMb|B-W2t+%=ZdiKGcQ*YeDTEap5HWu3-eni+6$F6Bd&4RrFow? zdH)Xn7RUP@=HG{sjzVSa$o%mYm$qZcBeY~EEivm;?g+F7Sz;6N4jVg^;7ILc*~+{Se!eYVwsJ~b1?=caAbD}s=*dYjJR{(5TF+o9lTKnimYTBIDQwK({{2A)MvVWKAe9gm9Kac`-C2;BQ*bV5uogG1jC|1*aB;q1a%r z2q92o3YiS#->TwVoqtr|E6P1}zjuE<|F)ms9~9me_s)kz46@A&kR1;3ncH8&XY|ha z4n;ytpFih-w1Z(j#0}_Qxg2I>O07q9w?co<3&D1_p-hO!;Adt-Oh&TdD;9WYc{-t<)PZlMwq?F zq+NX|;q7bPas}kiGFe;E#x~7t8KFFR36L?c0lT`!_@PR} zKI>vu9$0~64jEn`PF@FOmKE8-%({-?-9h%-@2f8%RiGYN!?Eh#ix+ zW62{(9t2{yiQ4Un4RR04JcMKi5Jd!HGj`|-YX zCDdy{g)T73(LU+c?USp%`jMU~-^$_puL|zrC(f(m^_O}I_09RV-C#3R?&Uw@c-KDu zeX)E-Trw#xIe&aoY#J3O#r1h%^Mn8-FRq^uCdJJ&0{*7;43?UrDaKz$dB)l)h!n(N zUg#LlZ0GA&8+0wEC@_$J8%Beh z^+CaH>S_49O@Mo*@|mi+frEZWj;E0iXA0`5FOg(TvGk%Io}syQBuJK#<;2V- z^%oqkHJEyR5i`&N@{X(rPf=0{{~?VGY~o%c`^7k~O%jxs{V<3#{zJEn?1P%smZvSUPc+oO8jGqKgd| ze7-EnVy&B!@cC-KgRHsENJ#&Dm5`NWRklR`e3h!JSgRao(}bJooGV*u^v}2h+}8-k zgdhF_IT5zv?`Gp$GaJR<3*x0l?`mrF`pLpv8Lav;YV_iplC-UW;GF4)Bw0h>loqV> z&EFK8?qX}Y%ve=qt#0&|lQ3z`mh0=Z;;z?eg`vr~&swKtUpq4E^pRQe=QuI|*5ezD zw#A(59`@6=@AgyMiocirbUmF7K(gmv_b?mn-_6GVY&HfP@hlBmT&IB=91W*DNW&=` zGU#m3C<-90!Bs4v5<4!ZQ)$KKx9K?uIFkef4&aas2(thSi6)YR5ehcXa_M9NRGZP1 zfi$>3Gv{|tGJpo+W(lA1HTj$9{L znm`f(BJW3n=Si-=i{vtrUja$vL4V8voN*!lBK+OPrT!BKv$wNgRpZx1c0B7m0LNE| zLr*ylaB=PcM@P=NoAnMmHpYbn+F?x~(LqHEup!ZdLS_m>MG6i|FgQHS94UV<`n*(p zsE0aLbH~Z_sftWN%Vq|`Z)vgA>;nA?8fu?Y9JnA>oI8A3TvDj2%LnS_q{=GJ`o5q& zdb*;Tm+>nSf3o@)4_zDjaktK#c~fUz?T)h;(T&+LqSg+(!yTVk`qwx1P5jKL{^K|G zoL_XuK|5R@c846+$jSIo-Az5|5I=OE;hTSbIH<@-6nCgKcRwuGXi7c+t;mOvd=CkR z5aq)_G$EEw^+L;c8Hlnib$@QCRPTs^2YeqihIkoWy3hixB$G<0IHfK4deP_%i_=ga zKZ>m{G~xj*a~>%z%g0cl)D)nv8UqE|p+xS$VwW6E#U-r_V?MU7X(jq=3PDdbc3i8P zqaEMgxv0Wy^uPu(h&ILY5ok43ZF?4c4BA!JHHtQPY2XxtHGD!IzsC7$@lDMq48jkleA z_4oLzI}cup-g0sct4FtuM#ml)caCn%FKeGr-hH63c=*h8~iR~ae?|#0p^B`ay zP-%GAXKwCMem|ezfAH52qoT+7tGfPG9w`LU*ngQ(~MzOd^MDtZhQ{g>C} zs{ACt1w-RUuGDQE-f=}(e64Eni}wxhy1H!T)yn#j?W2w3)#GZu>+n?N_x~gVVlW#q z2D98Vyz7dvl*M4YqpK%eQ~oV8!j`t&2#wP_v{BRY@87$~TjCCBnXgvMb0ms6>ea#(VTCK+#1rhtk=Iq0J7 z6%eh{wV0y7K>in>ZooI^i%$hy_qQ52SN9(k{G6ZJR(deZzg4;LV7>6x>T={aFFe>R zygh$g1ti{S;t#G7-f7|Np*Kgr(}doHemK2K;Wmc> zn!p)K9dNUsyS?CzBlspn+_ViKq%6+S95j09g+P?4#YM`&ttmIkth)u@&60PM3V^sw z2#-`k3N0ymkt#d-=tZUfMQF>vQRS)nheSFqi3 z-mHhlUwl~~09f@#QD1|>S$8r&>jMC*25Z>da;r`eSjvhFky$Wnk5J=p+-OmJ?Jwl<5+O&y(^AlCWjb7Rw8 zY-G&ycVw9knUnyyT5p5)?sjB~L3`H71ip4;8uXEA{IeVxS0Bc@=3ISfSkAd1aIlm# zzP7@8`v(_?nlcZ882h)0^_zlmJc4}x#$1k7JokXMT#y|d2 zcM?DT$pzb)J7yZ=P6&=69AcWSlDUMqV`TzBZa($Pn`d6XIdbOa&(0-2Z-Aq7XG?g) zb$R1W-O~tOk|nI+2J-P!uikulIONs6GyxsSo|3DD$rPqISYd%E7%F(fblYXg~6oEKhG@%d5t!@}!zf~Ao zBrDKwHvAdnyf_vMEXRXRYDAwAa7>0#$nrsaa6n6zVsJo9#@$mZQQ+iL1db^xbS%qM zba3?n#lfvLd;tZVlqj4gJT6Ocf-FhE8)Bhf^E9EYtlH|aJr|Bi!LhJ@Xgt%8r_11Y z+MwpqNARv)95?hoEfcMho%fQE7#z?snF1b z_hp5Kwe)^Xs6Kb-e9uVd6=5~?8ye1KUdsI_mlqn&cAm@SMKEIVH_g&4F@*`nniMLa zqkytdY_M>Q>|D;2F&U5m8~cT>XS>eVPKwJ%Iw!@|5U3q@(-`mSaW`1pEG$byypX|C zG{U=@@t0Aav33d~cGda%c51zMP1NUwF0|h3C&jKA0e{na21`xR6yq-oAkSDk1(AZ_ zduG=UcFhR&lS2K-j?vyr;A_=S3q3MIT%jt>6N!*?EE3VY#+gid1VCF5VFx-}*lLEo zqD6)yqHr=9&+Os}S=>H6dLDgAG%*GT4e>xQA_Gp)&o%`xf7#BUxtUJc+#%?ZLmJel z&NN+rauVe!0=FAD&AYijO)^Poi~J9eL@P@{1Lu~L=lQE#>u7&-72YM6yUQi1Ki9F-IIY^osaUHDB}$HxN|xhTZqgb8THA*Z0v0RIDq4!< zva`!7VwZqg7_gN~dPor;axl<~1o_YcMeAdZ1$wb88FCjb+M+EA_Y_1uv;hkAz1d$9 zGVP=T?wdEWZ{Gau%s1cT?{YbXgY@llnaYyDaeu`^kdnS~SOw(+j&LQ8@FY>?O9_@I zYeG4}bG1}S$XwT-84#~S8XlN zyK>2_>u=VVP0%y3hArha%g|oBqMOZX+3%Z;APo}dd3Y&-bF{;E4i}8E%{zPr-gc)p ztU^0U5^W*DISD7}2s^%2J4KRh(MdUClWR+kWIeJ|LFW2wTvQc&{p9l!WP&N?3RpY625lEVjcp>fN;UY2zp`j19^lx-sL`UJb=Q`!kn zX_tCRyKt8g=|iyv>)w`$Owx{Q@T`=!2ZI%e&0r{~i*hF&^cdtn;R*K+-%h}jghzlU z1&`Dg*SWm1;EIL9)GNh;n_OzHU)tu~gkf#-t+!5(G}fo{mhvNC8J-eAJ1EEis1R!7TsiH9l8adC?-*yMKSFP zjrH;IPy02?(y8q=EJ8!9F6I@A+tN*kUAoHo#<~q>P}QhAsFt;A6$aCwWwX}MsP1Or zwYt*&GldyNgjFzfS_n$$g} z!YQw5wyM9=sA_f1hBw=ao3(2VmCZ^eOEibW%V4M81jgfqV!SXFFTCsx)NfOWzk0=c z<@G&2(NUVT@VpI&>C-n~e|7O1ES$Ai(R6cV@uF_6+Gb;M&7`YVLo4fxYx>eRHDggT zRGVr!5ohz;%!*%P<2d)d<)^A9rv@6Bz_-4iBnO@p|74aDZ$t?Ap>_A+Nc-)e%m zmpKD@X7WJ5-vhtGD}{1bn7ldVmG&ZaAJjRo_CEFzs<*F&D!32zt){1TNoacrBj=8W z-gWkL`y--iX3QBPDBRLQWr$ab|$c? z909x?5FPnH3QXe=9a#~iZ7yg4#g=H&!hMfiZ7!W2GIpE^czs;zW5k$ zuxV3Q*9?2bsH+o`D|7;k`_aj7L399ukiK#G$CrP4^48SW)s4&h!bAuWMTCeVLPRkJ z5ydSVDi9*afWHTRg;xsYu24jXK#RRd-3JvQ0&DLJ&_}2uMEJD~B7A+z_Nu!i zw56jknR1+I;TK@+>^mTDWN+ji!_X3T2oguZfE{wK;QoF$x0#FB72)rM2c-plzMBO& z2@f`m!A~T4GYdRTxGo1w4}d38!Mmv|krWY$bVu$0sG~jRz+<}0;}HM>Of%IMhzQ(L zX^X%(fo~!h-N^)fv?Y>l%Z}v8z&g`T+WK$AZz@Ny8j^F;(03oUh_TLo8}~R%72N1u zsRPnN{#3!8-<6zjO@VbrmUuF{Qe+8`*8c?C2&#JfjG(HwT?AFV?Lw$}!uX_Mk3GRo z4(!OlhZ5tX3K=9rj^eY^jE$hdQp^)#NLO+)fu|t%*=d}aipWmkNw;NSsK*u_Axc4z zlmfm+t=6f4w~WQeidr||8HXSW%M%*0;ZJgoDKaJRDb@73`1)R;Q}X; z{7vqcxnK3OSv(cyR>vwp-9k2@fI zU^io4qIefkBr6mT5k<72xJy`gQD7Hlt0VN7@-;57UvewqJG8G3JYbM$>{PkC5&8S9qmR?h5lr4$xvRQujdx za)7n>1?VbNksSE7cZWh1dJ6TAQl8o+!6Q2f`9W7wRbrM^)s=%ll+iBtS=9?&$Gq0YD8N=V25`R`2l#-R0C+%60(?+y2KbQL0`TEQL2g}Ye?c=$ zLz>cwWK2$L>U>rsQYLR|nY5-)k3XXsv!*^bzMzv?V=g_ZjW1{u$I_YcbVe~rTGhs(yJ=9UxJ%PB z2Qw(MhDn-XT#7!b_t}q~U5l+LW zdKUpVgc?vqHF(8dv;t=$CA}=}ioIL~UJVrlT(H?Ljw=MLfbnnBUFI2k(VTwxRKr$a zidQ4pXG*vtvNpBJ#J8u{4Bf&`i9N5_#q*qT&@+C#k_t=ZKbrH7#M>j;NRK?PHmT8u z-{`>ZZ&Y3_;-&O?lQJc|LDVy1!0kslV`VY!JgX29k7iLb+HrxLIk}C`Vkzz zn@k(PP9_~?NH(x?Xgx~#g7uA#&MhJjDZruol*x29s}OCDOzOG0G|@_NXjUfXGg&i} zH%g(b4irY|VZ{Ij&Z?ADIhWNZ(pe>+&S|A4#Yj(SI6^7;6rDPw=f6sbPD=X~IKOgf zQ_(KWWz+ez2`%d(@ zH0SIagp-H#0bv4e<4pi?I7E}n=E}ELh1I84$yT)O;?wWP4z9Lu#SU$VoyB$l8?i&h z_RU!5O%Z=L>vSwu4HQM2!tDhJkdPL5(Y=!j z7NO#*J=^w9s+s#esR(u?`w=_l_lL5aV860wI7d;tNTJd}CKmk@glK^ua01gJjL zFlD_Aqun;rBz9g9^1MX}ALcM%}j<4SJEM-uOGC2V4PeFFMJHCLi;Lb;Nl zV*AN+<(v*0fz6s!Ru027I8_5&`B{~fc#=`A{?GTX)r$oIH2VM=LQ9~{fzB8%1TE1D zF2U%9kQK7{Ss1+#R>eXDD4VQ^)kGatutw+>yv(O&D&x!Nvh;xT8k86S(oW1G~|?_Hg} z!V>WN8ke%l67ZvqH26hL4bsINI)S49yK&49(6+&eOIS1hg377U2%U z9fCUycLMGR+)Z#t;f@vJ)T*dH6;Mo*2LWuiz=2E#0b3VJ{7G_(VoOK594kUPLM;L? zHDu`+MZuVmrIQrx1#@HR7_~P5gG#$w9~4->-T|cUX4EGDFGU;|!+MyYT|*v4AOR?e zV3DLK?4xji!V`C!J*G(?OoY{aDX(!tI&hFq1Tuoh5gbO)h2RK+ZUE~oOc5Cc+-oqT z$T1}OGJ>9}Hrsw+?=Zyd99HP!g2>3~&}OV3uwoj(S_j3}I^LKD|HC=RN!tKHkIu=8r)9MgIuj4B)6#B1$Zie#!dbO3I zweUT$zg8Q=vw@!bDyXbA-W=-Bsq4)NGH=d&W4@da_2q=Cd^wSZJttnzjL*qht6ANb zFDG&j-X-q|?(;@v4^71h?xPi!DbYsr$QnOd_q4A==gK%W>iTkGYW$P;<(LOxFVU;~ z9fgpZ0C}G*@F1aiD(k`DXN9PA7u06Z1Cm}n0OWU+w#6&~B!A4f>G?W8$=?FnMhqnX zJ{;AG`bvJk)eAe)#WTJiox93vYr{LV-hK!H_2$oHxeYiu7H}lDkT1Q$g-ZZt6vllT3V5Vci2G zF^Qc*tQWy3g0Ca^CV&c=o2n>P_zrF1Y)fEUjN7F_1NTi?_9DjQ@1s8QcUsS8AZkIS zFHxZ*x;TvQQEB@R+iS?L_N~6O6@AR>H9TDG+z=0=e*#>Ghi{7byJ1(bk_$J*!|0zt zimfv0HeVADqf^3ecF^l|ELIH^Mes`6v$zNh_P$5@CpN_H6%oL4-->v77?I(X3!AZS zz|fh}T?Ewtu%h{f49Lobq702UMf(R;NMDp$%@&Ts&}%rZOUDp8fJ?b@fuYxNe1<}& zBB%xB@m>5qvAb4~?}cSx+tmvHJ!j`kqt4Exb8JIcCMh@155a<_hM!2K;3w@AU-{_Q zuSkYAspnPc??3ycbUd~5yJe}1`9Y*xzkmDI)l1UOTbFiz^HaIUbHr0eDF*78O9t(( z23#^{ckz@S@i~Pca^iKCPU!P_HBA=DIhgJ!+_r1RIgl`nQGFooIZR=l*tHjMW$WPQ zx-BcWRc;nPWy@6`>nLw!CH>O~xK?vEdOUgupXmP>&hAe!)QL~b^|RFc=v+(9b9ngCeY(C^JUs5j_ll~+C@gPZ|HmFMs+LxzB|Wc6 z=(3Z#KK%KcJFmYir3RpfbnA~V@4R)fR)wIk9BRkWSS!^d(4r$xBET8RQwYu=cox7} z@*JXH18|QnMNlT@^lTQ~hsr`Guj&hmmRHM1P7X#y<;Zp^mURmE$nh8z|9>vUJxc`k z#-1eti$E{W;dQ^32)af3`p^vd=ogoFUVlgGI{Td$q@7pZ0h2?f#t5h^QrF;zf4Ee0 zZjkRn=X;6<@1~mgC28a#hg3VSVYES^;b0wR9EICQC8|oN#YV#d4JUc- z1qNWxaK7mx6*kA`BQStrStrReAt&dNhf`>CRPyjomOuRMpX%~375fb)bL_^xl89ni##+E5M4!=Nb{yN7D*ybCu}L&k9U?<2rC z3^NMkcsVD6#ZD@7A!sOtDA?%-2+{~95KICvFv#l-kFem#V?Kuqu91S44hxSM&=I3F z#=8J6a%IcG^+tLuhh_{BVAx@KV5O}(908dJ-D{9=&|U0UYi%?fG2liPR_q{y8&kl! z>=*2Njl1-@|fA zAjQ3hXbU7`LTbEQRSMg6EWrg&vQmikE44cR+0x_wI}1y-rD3%Ykg_NnI)rGhEWy+~ zngc>ZQ`8+y&}Br>h$E<}5j^q<{$E^#-wM?T;Z|Kv_@a;NWvcz%?>9TE7)Z$|~b69Hf%KX5x4;*Z{L4M1Hy0Cn*o P)U{9yz#S+arStw5pTx!$ literal 0 HcmV?d00001 diff --git a/ai_trade/tests/__pycache__/test_signal_generator.cpython-312-pytest-9.0.2.pyc b/ai_trade/tests/__pycache__/test_signal_generator.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8399ee4fb8631d7a976d9dbb4f802a2d3a522ce GIT binary patch literal 16510 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 yg|+#YxSQ9$6&KWZ2Fmkt3UjNl(hs&|(F literal 0 HcmV?d00001 diff --git a/signal_v2/tests/conftest.py b/ai_trade/tests/conftest.py similarity index 81% rename from signal_v2/tests/conftest.py rename to ai_trade/tests/conftest.py index 73146c7..4ff14b8 100644 --- a/signal_v2/tests/conftest.py +++ b/ai_trade/tests/conftest.py @@ -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 diff --git a/signal_v2/tests/test_chronos_predictor.py b/ai_trade/tests/test_chronos_predictor.py similarity index 93% rename from signal_v2/tests/test_chronos_predictor.py rename to ai_trade/tests/test_chronos_predictor.py index d8b6210..804044b 100644 --- a/signal_v2/tests/test_chronos_predictor.py +++ b/ai_trade/tests/test_chronos_predictor.py @@ -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 diff --git a/signal_v2/tests/test_kis_client.py b/ai_trade/tests/test_kis_client.py similarity index 99% rename from signal_v2/tests/test_kis_client.py rename to ai_trade/tests/test_kis_client.py index 52b4398..125af6e 100644 --- a/signal_v2/tests/test_kis_client.py +++ b/ai_trade/tests/test_kis_client.py @@ -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 diff --git a/signal_v2/tests/test_kis_websocket.py b/ai_trade/tests/test_kis_websocket.py similarity index 98% rename from signal_v2/tests/test_kis_websocket.py rename to ai_trade/tests/test_kis_websocket.py index 82f18b8..b9fb26c 100644 --- a/signal_v2/tests/test_kis_websocket.py +++ b/ai_trade/tests/test_kis_websocket.py @@ -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" diff --git a/signal_v2/tests/test_main.py b/ai_trade/tests/test_main.py similarity index 74% rename from signal_v2/tests/test_main.py rename to ai_trade/tests/test_main.py index 851aeb0..c0b0ffa 100644 --- a/signal_v2/tests/test_main.py +++ b/ai_trade/tests/test_main.py @@ -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) diff --git a/signal_v2/tests/test_momentum_classifier.py b/ai_trade/tests/test_momentum_classifier.py similarity index 98% rename from signal_v2/tests/test_momentum_classifier.py rename to ai_trade/tests/test_momentum_classifier.py index a23e9d2..e48e162 100644 --- a/signal_v2/tests/test_momentum_classifier.py +++ b/ai_trade/tests/test_momentum_classifier.py @@ -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, ) diff --git a/signal_v2/tests/test_pull_worker.py b/ai_trade/tests/test_pull_worker.py similarity index 92% rename from signal_v2/tests/test_pull_worker.py rename to ai_trade/tests/test_pull_worker.py index 220f58e..6f9ae5c 100644 --- a/signal_v2/tests/test_pull_worker.py +++ b/ai_trade/tests/test_pull_worker.py @@ -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": [{ diff --git a/signal_v2/tests/test_rate_limit.py b/ai_trade/tests/test_rate_limit.py similarity index 84% rename from signal_v2/tests/test_rate_limit.py rename to ai_trade/tests/test_rate_limit.py index 9cea492..dd62350 100644 --- a/signal_v2/tests/test_rate_limit.py +++ b/ai_trade/tests/test_rate_limit.py @@ -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 diff --git a/signal_v2/tests/test_scheduler.py b/ai_trade/tests/test_scheduler.py similarity index 97% rename from signal_v2/tests/test_scheduler.py rename to ai_trade/tests/test_scheduler.py index 245ce3d..f403afe 100644 --- a/signal_v2/tests/test_scheduler.py +++ b/ai_trade/tests/test_scheduler.py @@ -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): diff --git a/signal_v2/tests/test_signal_generator.py b/ai_trade/tests/test_signal_generator.py similarity index 98% rename from signal_v2/tests/test_signal_generator.py rename to ai_trade/tests/test_signal_generator.py index 00e9ad2..663efd7 100644 --- a/signal_v2/tests/test_signal_generator.py +++ b/ai_trade/tests/test_signal_generator.py @@ -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): diff --git a/signal_v2/tests/test_stock_client.py b/ai_trade/tests/test_stock_client.py similarity index 94% rename from signal_v2/tests/test_stock_client.py rename to ai_trade/tests/test_stock_client.py index b3ceab3..4567139 100644 --- a/signal_v2/tests/test_stock_client.py +++ b/ai_trade/tests/test_stock_client.py @@ -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( diff --git a/signal_v2/tests/test_stock_client_ttl.py b/ai_trade/tests/test_stock_client_ttl.py similarity index 94% rename from signal_v2/tests/test_stock_client_ttl.py rename to ai_trade/tests/test_stock_client_ttl.py index 1ab2fba..a4cc258 100644 --- a/signal_v2/tests/test_stock_client_ttl.py +++ b/ai_trade/tests/test_stock_client_ttl.py @@ -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(): diff --git a/start.bat b/legacy/start_v1.bat similarity index 100% rename from start.bat rename to legacy/start_v1.bat diff --git a/signal_v1/DEPRECATED.md b/signal_v1/DEPRECATED.md new file mode 100644 index 0000000..b13f118 --- /dev/null +++ b/signal_v1/DEPRECATED.md @@ -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` diff --git a/signal_v2/start.bat b/signal_v2/start.bat deleted file mode 100644 index 399ac9e..0000000 --- a/signal_v2/start.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -cd /d "%~dp0\.." -python -m uvicorn signal_v2.main:app --host 0.0.0.0 --port 8001