From 9dbf6e6791d041b951780b22f3c0363d60e39667 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 14 Feb 2026 18:03:13 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=98=EB=B3=B5=EC=A0=81=EC=9D=B8=20IPC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0,=20=EB=B4=87=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0,=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0,=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=9E=90=EC=9B=90=20=ED=95=A0=EB=8B=B9=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0,=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 113 +++---- backtester.py | 7 +- main_server.py | 165 +++++---- modules/analysis/deep_learning.py | 423 ++++++++++++++++++------ modules/bot.py | 387 +++++++++++----------- modules/config.py | 22 +- modules/services/kis.py | 165 ++++++++- modules/services/news.py | 60 +++- modules/services/ollama.py | 30 +- modules/services/telegram_bot/runner.py | 63 ++-- modules/services/telegram_bot/server.py | 330 +++++++++--------- modules/strategy/process.py | 6 +- modules/utils/ipc.py | 234 +++++++------ modules/utils/monitor.py | 67 ++-- modules/utils/process_tracker.py | 227 +++++++++---- 15 files changed, 1452 insertions(+), 847 deletions(-) diff --git a/README.md b/README.md index 5243a9b..04b62c9 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ # ๐Ÿค– AI Automated Trading System (Windows Server Edition) ์ด ํ”„๋กœ์ ํŠธ๋Š” **Python, PyTorch (Deep Learning), Ollama (LLM)**์„ ํ™œ์šฉํ•˜์—ฌ ํ•œ๊ตญ ์ฃผ์‹ ์‹œ์žฅ(KIS)์—์„œ ์ž๋™์œผ๋กœ ๋งค๋งค๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ณ ์„ฑ๋Šฅ AI ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡์ž…๋‹ˆ๋‹ค. +**ProcessPoolExecutor ๊ธฐ๋ฐ˜์˜ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ**์™€ **๋…๋ฆฝ๋œ ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ ํ”„๋กœ์„ธ์Šค**๋ฅผ ํ†ตํ•ด ๋†’์€ ์•ˆ์ •์„ฑ๊ณผ ์‘๋‹ต ์†๋„๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ## ๐Ÿš€ Key Features -* **Multi-Process Architecture**: ๋ฉ”์ธ ์„œ๋ฒ„(`main_server.py`)๊ฐ€ ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡๊ณผ ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์„ ๋…๋ฆฝ๋œ ํ”„๋กœ์„ธ์Šค๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์•ˆ์ •์„ฑ ํ™•๋ณด. -* **Advanced AI Analysis**: **RTX 5070 Ti (16GB VRAM)** ํ•˜๋“œ์›จ์–ด ๊ฐ€์†์„ ํ™œ์šฉํ•œ ๊ณ ์„ฑ๋Šฅ ์˜ˆ์ธก ๋ชจ๋ธ ํƒ‘์žฌ. -* **Hybrid Strategy**: ๊ธฐ์ˆ ์  ๋ถ„์„ + ๋‰ด์Šค ๊ฐ์„ฑ ๋ถ„์„(LLM) + ๊ฐ€๊ฒฉ ์˜ˆ์ธก(LSTM)์„ ๊ฒฐํ•ฉํ•œ ๋ณตํ•ฉ ์ถ”๋ก . -* **Telegram Control**: ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ์กฐํšŒ, ๋ฆฌํฌํŠธ ์ˆ˜์‹ , ํ”„๋กœ์„ธ์Šค ์ œ์–ด(`/restart` ๋“ฑ) ์ง€์›. -* **Safe Trading**: ์˜ˆ์ˆ˜๊ธˆ ์ดˆ๊ณผ ๋งค์ˆ˜ ๋ฐฉ์ง€, ์†์ ˆ/์ต์ ˆ ์ž๋™ํ™”, API ์†๋„ ์ œํ•œ(Throttling) ์ ์šฉ. +* **Multi-Process Architecture**: ๋ฉ”์ธ ์„œ๋ฒ„, ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡, ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์ด ๊ฐ๊ฐ ๋…๋ฆฝ๋œ ํ”„๋กœ์„ธ์Šค๋กœ ์‹คํ–‰๋˜์–ด ์ƒํ˜ธ ๊ฐ„์„ญ์„ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค. +* **Advanced AI Analysis**: **RTX 5070 Ti (16GB VRAM)** ํ•˜๋“œ์›จ์–ด ๊ฐ€์†์„ ํ™œ์šฉํ•œ **Attention-LSTM** ๋ชจ๋ธ์ด ์ฃผ๊ฐ€๋ฅผ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค. +* **Process Management System**: + * **Zombie Killer**: ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ด์ „์— ์ข…๋ฃŒ๋˜์ง€ ์•Š์€ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * **PID Tracking**: ์‹คํ–‰ ์ค‘์ธ ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค์˜ ID๋ฅผ `pids.txt` ํŒŒ์ผ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ธฐ๋กํ•˜์—ฌ ์‹๋ณ„์„ ๋•์Šต๋‹ˆ๋‹ค. +* **Reliable Telegram Bot**: + * **HTML Parsing**: ๋งˆํฌ๋‹ค์šด ์—๋Ÿฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์•ˆ์ •์ ์ธ HTML ํฌ๋งท์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. + * **Interactive Commands**: `/status`, `/portfolio`, `/exec` ๋“ฑ ๋‹ค์–‘ํ•œ ๋ช…๋ น์–ด๋กœ ๋ด‡์„ ์‹ค์‹œ๊ฐ„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +* **Auto-Recovery**: `ProcessPoolExecutor`์˜ ์›Œ์ปค ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ถฉ๋Œ(OOM ๋“ฑ)ํ•  ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. --- @@ -16,28 +21,27 @@ ```plaintext / -โ”œโ”€โ”€ main_server.py # [Entry Point] ํ”„๋กœ์„ธ์Šค ๋งค๋‹ˆ์ € ๋ฐ ์„œ๋ฒ„ ์‹คํ–‰ -โ”œโ”€โ”€ watchlist_manager.py # [Automation] ๋งค์ผ ์•„์นจ ์ข…๋ชฉ ์„ ์ • ๋ฐ Watchlist ๊ฐฑ์‹  +โ”œโ”€โ”€ main_server.py # [Entry Point] ํ”„๋กœ์„ธ์Šค ๋งค๋‹ˆ์ € ๋ฐ FastAPI ์„œ๋ฒ„ +โ”œโ”€โ”€ pids.txt # [Runtime] ์‹คํ–‰ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค ID ๋ชฉ๋ก (์ž๋™ ๊ด€๋ฆฌ) โ”œโ”€โ”€ modules/ -โ”‚ โ”œโ”€โ”€ bot.py # [Core] ๋ฉ”์ธ ํŠธ๋ ˆ์ด๋”ฉ ๋ฃจํ”„ ๋ฐ ์‚ฌ์ดํด ๊ด€๋ฆฌ +โ”‚ โ”œโ”€โ”€ bot.py # [Core] ๋ฉ”์ธ ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡ (์Šค์ผ€์ค„๋Ÿฌ & ์ƒํƒœ ๋จธ์‹ ) โ”‚ โ”œโ”€โ”€ config.py # [Config] ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ์ƒ์ˆ˜ ๊ด€๋ฆฌ -โ”‚ โ”œโ”€โ”€ analysis/ # [Brain] ๋ถ„์„ ๋ชจ๋“ˆ +โ”‚ โ”œโ”€โ”€ analysis/ # [Brain] AI ๋ถ„์„ ๋ชจ๋“ˆ โ”‚ โ”‚ โ”œโ”€โ”€ deep_learning.py # PyTorch ๊ธฐ๋ฐ˜ Attention-LSTM ๋ชจ๋ธ โ”‚ โ”‚ โ”œโ”€โ”€ technical.py # RSI, ๋ณผ๋ฆฐ์ €๋ฐด๋“œ ๋“ฑ ๋ณด์กฐ์ง€ํ‘œ ๊ณ„์‚ฐ โ”‚ โ”‚ โ””โ”€โ”€ macro.py # ๊ฑฐ์‹œ๊ฒฝ์ œ(ํ™˜์œจ, ์œ ๊ฐ€, ์ง€์ˆ˜) ๋ถ„์„ โ”‚ โ”œโ”€โ”€ services/ # [I/O] ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™ -โ”‚ โ”‚ โ”œโ”€โ”€ kis.py # ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ API ํด๋ผ์ด์–ธํŠธ (Throttling ์ ์šฉ) -โ”‚ โ”‚ โ”œโ”€โ”€ telegram.py # ํ…”๋ ˆ๊ทธ๋žจ ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก +โ”‚ โ”‚ โ”œโ”€โ”€ kis.py # ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ API (Throttling ์ ์šฉ) +โ”‚ โ”‚ โ”œโ”€โ”€ telegram_bot/ # [Independent] ๋…๋ฆฝ ํ”„๋กœ์„ธ์Šค ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ โ”‚ โ”‚ โ”œโ”€โ”€ news.py # ๋„ค์ด๋ฒ„ ๋‰ด์Šค ํฌ๋กค๋ง -โ”‚ โ”‚ โ”œโ”€โ”€ ollama.py # Local LLM (Llama 3) ์ธํ„ฐํŽ˜์ด์Šค -โ”‚ โ”‚ โ””โ”€โ”€ telegram_bot/ # ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ ์„œ๋ฒ„ (Interactive Mode) -โ”‚ โ”œโ”€โ”€ strategy/ # [Logic] ๋งค์ˆ˜/๋งค๋„ ์˜์‚ฌ๊ฒฐ์ • ๋กœ์ง -โ”‚ โ””โ”€โ”€ utils/ # [Util] IPC, ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋“ฑ -โ”œโ”€โ”€ data/ # [Runtime Data] ์‹คํ–‰ ์ค‘ ์ƒ์„ฑ๋˜๋Š” ๋ฐ์ดํ„ฐ (Git ์ œ์™ธ) -โ”‚ โ”œโ”€โ”€ bot_ipc.json # ํ”„๋กœ์„ธ์Šค ๊ฐ„ ์ƒํƒœ ๊ณต์œ  -โ”‚ โ”œโ”€โ”€ watchlist.json # ๊ฐ์‹œ ๋Œ€์ƒ ์ข…๋ชฉ ๋ฆฌ์ŠคํŠธ -โ”‚ โ””โ”€โ”€ daily_trade_history.json # ๋‹น์ผ ๋งค๋งค ๊ธฐ๋ก -โ””โ”€โ”€ tests/ # ํ…Œ์ŠคํŠธ ์Šคํฌ๋ฆฝํŠธ +โ”‚ โ”‚ โ””โ”€โ”€ ollama.py # Local LLM (Llama 3) ์ธํ„ฐํŽ˜์ด์Šค +โ”‚ โ”œโ”€โ”€ strategy/ # [Logic] ๋งค์ˆ˜/๋งค๋„ ์˜์‚ฌ๊ฒฐ์ • ํ”„๋กœ์„ธ์Šค +โ”‚ โ”‚ โ””โ”€โ”€ process.py # ์›Œ์ปค ํ”„๋กœ์„ธ์Šค์šฉ ๋ถ„์„ ํ•จ์ˆ˜ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) +โ”‚ โ””โ”€โ”€ utils/ # [Util] ์œ ํ‹ธ๋ฆฌํ‹ฐ +โ”‚ โ”œโ”€โ”€ process_tracker.py # PID ์ถ”์  ๋ฐ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค ์ •๋ฆฌ +โ”‚ โ”œโ”€โ”€ ipc.py # ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹  (IPC) +โ”‚ โ””โ”€โ”€ monitor.py # ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง +โ””โ”€โ”€ ... ``` --- @@ -56,60 +60,45 @@ ### 2. Hardware Acceleration (RTX 5070 Ti) * **CUDA Optimization**: PyTorch๋ฅผ ํ†ตํ•ด GPU ๊ฐ€์† ํ™œ์„ฑํ™”. -* **Specs**: - * Batch Size: 64 - * Epochs: 200 - * Precision: FP32 +* **Specs**: Batch Size 64, Epochs 200, Precision FP32. * ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ `High Performance Mode`๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ์ง€ ๋ฐ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. - -### 3. Integrated Decision Making (Quant Strategy) -AI ๋ชจ๋ธ์˜ ์˜ˆ์ธก ๊ฒฐ๊ณผ๋Š” ๋‹จ๋…์œผ๋กœ ์“ฐ์ด์ง€ ์•Š๊ณ , ๋‹ค์Œ ์š”์†Œ๋“ค๊ณผ ๊ฒฐํ•ฉ๋˜์–ด ์ตœ์ข… ๋งค์ˆ˜ ๊ฒฐ์ •์„ ๋‚ด๋ฆฝ๋‹ˆ๋‹ค. -1. **Technical Score (40%)**: RSI, ๊ฑฐ๋ž˜๋Ÿ‰, ๋ณ€๋™์„ฑ ์ง€ํ‘œ. -2. **News Sentiment (30%)**: Ollama(LLM)๊ฐ€ ๋ถ„์„ํ•œ ์ตœ์‹  ๋‰ด์Šค ๊ธ/๋ถ€์ • ์ ์ˆ˜. -3. **AI Prediction (30% ~ 60%)**: LSTM ๋ชจ๋ธ์˜ ์ƒ์Šน ์˜ˆ์ธก ์‹ ๋ขฐ๋„. - * *AI Confidence*๊ฐ€ 85% ์ด์ƒ์ผ ๊ฒฝ์šฐ, AI ๋น„์ค‘์„ **60%**๊นŒ์ง€ ๋™์ ์œผ๋กœ ์ƒํ–ฅ ์กฐ์ •. +* **OOM Protection**: GPU ๋ฉ”๋ชจ๋ฆฌ ๋ณดํ˜ธ๋ฅผ ์œ„ํ•ด ๋ณ‘๋ ฌ ์›Œ์ปค ์ˆ˜๋ฅผ 2๊ฐœ๋กœ ์ œํ•œํ•˜๊ณ , ์›Œ์ปค ์ถฉ๋Œ ์‹œ ์ž๋™ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. --- -## ๐Ÿ› ๏ธ Setup & Installation +## ๐Ÿ› ๏ธ Usage & Troubleshooting -### 1. Prerequisites -* Python 3.10+ -* NVIDIA GPU + CUDA Toolkit (Recommended for AI performance) -* Ollama (Local LLM Server running on port 11434) - -### 2. Installation +### 1. Installation ```bash -# Clone Repository +# Clone & Install git clone - -# Install Dependencies pip install -r requirements.txt -``` -### 3. Configuration (.env) -ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— `.env` ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ์•„๋ž˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. -```ini -# ํ•œ๊ตญํˆฌ์ž์ฆ๊ถŒ (KIS) -KIS_APP_KEY=your_app_key -KIS_APP_SECRET=your_app_secret -KIS_ACCOUNT=12345678-01 -KIS_IS_VIRTUAL=True # True: ๋ชจ์˜ํˆฌ์ž, False: ์‹ค์ „ํˆฌ์ž - -# Telegram -TELEGRAM_BOT_TOKEN=your_bot_token -TELEGRAM_CHAT_ID=your_chat_id - -# AI Server -OLLAMA_API_URL=http://localhost:11434 -``` - -### 4. Run -```bash +# Start Server python main_server.py ``` +### 2. Process Management (`pids.txt`) +์„œ๋ฒ„๊ฐ€ ์‹คํ–‰๋˜๋ฉด `pids.txt` ํŒŒ์ผ์— ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค ๋ชฉ๋ก์ด ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. +```text +58360: Main Server (Uvicorn Worker) +72028: Trading Bot Main +66488: Telegram Bot Standalone +16372: Trading Bot Worker +... +``` +* **CPU ์‚ฌ์šฉ๋Ÿ‰์ด ๋น„์ •์ƒ์ ์œผ๋กœ ๋†’์„ ๋•Œ**: ์ž‘์—… ๊ด€๋ฆฌ์ž๋‚˜ `Get-Process python`์œผ๋กœ ํ™•์ธํ•œ PID๊ฐ€ `pids.txt`์— ์—†๋‹ค๋ฉด **์ข€๋น„ ํ”„๋กœ์„ธ์Šค**์ž…๋‹ˆ๋‹ค. +* **์ž๋™ ์ •๋ฆฌ**: `main_server.py`๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๋ฉด ์‹œ์ž‘ ์‹œ ์ž๋™์œผ๋กœ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ฐพ์•„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. + +### 3. Telegram Commands +* `/start`: ๋ด‡ ์‹œ์ž‘ ๋ฐ ๋ช…๋ น์–ด ์•ˆ๋‚ด +* `/status`: ํ˜„์žฌ ๋ด‡ ์ƒํƒœ, ์‹œ์žฅ ์ง€์ˆ˜, AI ๋ชจ๋ธ ์ƒํƒœ ์กฐํšŒ +* `/portfolio`: ํ˜„์žฌ ๋ณด์œ  ์ข…๋ชฉ ๋ฐ ์ˆ˜์ต๋ฅ  ์กฐํšŒ +* `/system`: CPU/GPU ์‚ฌ์šฉ๋Ÿ‰ ๋ฐ ํ”„๋กœ์„ธ์Šค ์ƒํƒœ ํ™•์ธ +* `/restart`: ๋ด‡ ํ”„๋กœ์„ธ์Šค ์žฌ์‹œ์ž‘ (์—…๋ฐ์ดํŠธ ๋ฐ˜์˜ ์‹œ ์œ ์šฉ) +* `/stop`: ๋ด‡ ์ข…๋ฃŒ + --- ## โš ๏ธ Disclaimer -๋ณธ ์†Œํ”„ํŠธ์›จ์–ด๋Š” ํˆฌ์ž๋ฅผ ๋ณด์กฐํ•˜๋Š” ๋„๊ตฌ์ด๋ฉฐ, ํˆฌ์ž์˜ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ฑ…์ž„์€ ์ „์ ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค. AI์˜ ์˜ˆ์ธก์€ 100% ์ •ํ™•ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์‹œ์žฅ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์†์‹ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. +๋ณธ ์†Œํ”„ํŠธ์›จ์–ด๋Š” ํˆฌ์ž๋ฅผ ๋ณด์กฐํ•˜๋Š” ๋„๊ตฌ์ด๋ฉฐ, ํˆฌ์ž์˜ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์ฑ…์ž„์€ ์ „์ ์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ์žˆ์Šต๋‹ˆ๋‹ค. AI์˜ ์˜ˆ์ธก์€ 100% ์ •ํ™•ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์‹œ์žฅ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์†์‹ค์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ์˜ํˆฌ์ž ํ™˜๊ฒฝ์—์„œ ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ ํ›„ ์‚ฌ์šฉํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. diff --git a/backtester.py b/backtester.py index 415d74b..251d69e 100644 --- a/backtester.py +++ b/backtester.py @@ -2,9 +2,8 @@ import pandas as pd import numpy as np import yfinance as yf import matplotlib.pyplot as plt -from analysis_module import TechnicalAnalyzer -from ai_predictor import PricePredictor -import torch +from modules.analysis.technical import TechnicalAnalyzer +from modules.analysis.deep_learning import PricePredictor class Backtester: def __init__(self, ticker, start_date, end_date, initial_capital=10000000): @@ -92,7 +91,7 @@ class Backtester: current_window_list = list(history_window) # 1. ๊ธฐ์ˆ ์  ๋ถ„์„ - tech_score, rsi, volatility = TechnicalAnalyzer.get_technical_score(today_price, current_window_list) + tech_score, rsi, volatility, vol_ratio, ma_info = TechnicalAnalyzer.get_technical_score(today_price, current_window_list) # 2. AI ์˜ˆ์ธก (Online Learning Simulation) # ๋งค์ผ ์žฌํ•™์Šตํ•˜๋ฉด ๋„ˆ๋ฌด ๋А๋ฆฌ๋ฏ€๋กœ, 5์ผ์— ํ•œ๋ฒˆ์”ฉ๋งŒ ํ•™์Šตํ•œ๋‹ค๊ณ  ๊ฐ€์ • (ํƒ€ํ˜‘) diff --git a/main_server.py b/main_server.py index 3c52119..82f2962 100644 --- a/main_server.py +++ b/main_server.py @@ -1,11 +1,7 @@ -import os import uvicorn -import subprocess -import sys +import multiprocessing from fastapi import FastAPI, Request from pydantic import BaseModel -from typing import List, Optional -from datetime import datetime from contextlib import asynccontextmanager from modules.config import Config @@ -13,100 +9,121 @@ from modules.services.ollama import OllamaManager from modules.services.kis import KISClient from modules.services.news import NewsCollector from modules.services.telegram import TelegramMessenger +from modules.bot import AutoTradingBot +from modules.utils.process_tracker import ProcessTracker, ProcessWatchdog +from modules.services.telegram_bot.runner import run_telegram_bot_standalone # ์ „์—ญ ๊ฐ์ฒด -bot_process = None -telegram_process = None messenger = TelegramMessenger() ai_agent = None kis_client = None news_collector = None +watchdog = None -import multiprocessing -from modules.bot import AutoTradingBot -from modules.utils.process_tracker import ProcessTracker -from modules.services.telegram_bot.runner import run_telegram_bot_standalone -# ๋ด‡ ์‹คํ–‰ ๋ž˜ํผ ํ•จ์ˆ˜ -def run_trading_bot(): +def run_trading_bot(ipc_lock, command_queue, shutdown_event): + """ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡ ์‹คํ–‰ ๋ž˜ํผ""" ProcessTracker.register("Trading Bot Main") - bot = AutoTradingBot() + bot = AutoTradingBot(ipc_lock=ipc_lock, command_queue=command_queue, + shutdown_event=shutdown_event) bot.loop() + @asynccontextmanager async def lifespan(app: FastAPI): - # [Startup] - global bot_process, telegram_process, messenger, ai_agent, kis_client, news_collector - + global ai_agent, kis_client, news_collector, watchdog + # 1. ์„ค์ • ๊ฒ€์ฆ Config.validate() - - # 2. ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” (์„œ๋ฒ„์šฉ) - # [Process Tracker] ์ดˆ๊ธฐํ™” + + # 2. ์ข€๋น„ ํ”„๋กœ์„ธ์Šค ์ •๋ฆฌ try: + ProcessTracker.check_and_kill_zombies() ProcessTracker.clear() ProcessTracker.register("Main Server (Uvicorn Worker)") - except: pass + except Exception: + pass + # 3. ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ai_agent = OllamaManager() kis_client = KISClient() news_collector = NewsCollector() - print("๐Ÿค– Starting AI Trading Bot & Telegram Bot (Multimedia Mode)...") - - # 3. ๋ฉ€ํ‹ฐํ”„๋กœ์„ธ์Šค ์‹คํ–‰ - # (1) ํŠธ๋ ˆ์ด๋”ฉ ๋ด‡ - bot_process = multiprocessing.Process(target=run_trading_bot) - bot_process.start() - - # (2) ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ (Polling) - telegram_process = multiprocessing.Process(target=run_telegram_bot_standalone) - telegram_process.start() - - # [Process Tracker] ์ž์‹ ํ”„๋กœ์„ธ์Šค PID ๊ธฐ๋ก (๋ถ€๋ชจ ๊ด€์ ) - try: - with open(ProcessTracker.FILE_PATH, "a", encoding="utf-8") as f: - f.write(f"{bot_process.pid}: Trading Bot Process (Parent View)\n") - f.write(f"{telegram_process.pid}: Telegram Bot Process (Parent View)\n") - except: pass - - messenger.send_message("๐Ÿ–ฅ๏ธ **[Server Started]** Windows AI Server (Refactored) Online.") - - yield - - # [Shutdown] - print("๐Ÿ›‘ Shutting down processes...") - - if telegram_process and telegram_process.is_alive(): - print(" - Stopping Telegram Bot...") - telegram_process.join(timeout=5) - if telegram_process.is_alive(): - telegram_process.terminate() - telegram_process.join() + # 4. ๊ณต์œ  ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ + ipc_lock = multiprocessing.Lock() + command_queue = multiprocessing.Queue() + shutdown_event = multiprocessing.Event() + + print("[Server] Starting AI Trading Bot & Telegram Bot...") + + # 5. ์ž์‹ ํ”„๋กœ์„ธ์Šค ์ƒ์„ฑ + bot_args = (ipc_lock, command_queue, shutdown_event) + telegram_args = (ipc_lock, command_queue, shutdown_event) + + bot_process = multiprocessing.Process( + target=run_trading_bot, args=bot_args) + bot_process.start() + + telegram_process = multiprocessing.Process( + target=run_telegram_bot_standalone, args=telegram_args) + telegram_process.start() + + # 6. Watchdog ์‹œ์ž‘ + watchdog = ProcessWatchdog(shutdown_event=shutdown_event) + watchdog.watch("Trading Bot", bot_process, run_trading_bot, bot_args) + watchdog.watch("Telegram Bot", telegram_process, + run_telegram_bot_standalone, telegram_args) + watchdog.start() + + messenger.send_message("[Server Started] Windows AI Server Online.") + + yield + + # [Shutdown] + print("[Server] Shutting down...") + shutdown_event.set() + + if watchdog: + watchdog.stop() + + # ์ž์‹ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ + for name in ["Trading Bot", "Telegram Bot"]: + proc = watchdog.get_process(name) if watchdog else None + if proc and proc.is_alive(): + print(f" - Stopping {name}...") + proc.join(timeout=5) + if proc.is_alive(): + proc.terminate() + proc.join(timeout=3) + + # SharedMemory ์ •๋ฆฌ + try: + from multiprocessing.shared_memory import SharedMemory + shm = SharedMemory(name=Config.SHM_NAME, create=False) + shm.close() + shm.unlink() + except Exception: + pass + + messenger.send_message("[Server Stopped] Server Shutting Down.") - if bot_process and bot_process.is_alive(): - print(" - Stopping Trading Bot...") - bot_process.join(timeout=5) - if bot_process.is_alive(): - bot_process.terminate() - bot_process.join() - - messenger.send_message("๐Ÿ›‘ **[Server Stopped]** Server Shutting Down.") app = FastAPI(title="Windows AI Stock Server", lifespan=lifespan) + @app.middleware("http") async def log_requests(request: Request, call_next): - print(f"๐Ÿ“ฅ {request.method} {request.url}") + print(f"[HTTP] {request.method} {request.url}") response = await call_next(request) return response -# ๋ชจ๋ธ ์ •์˜ + class ManualOrderRequest(BaseModel): ticker: str - action: str # BUY, SELL + action: str quantity: int + @app.get("/") def index(): vram = 0 @@ -118,6 +135,7 @@ def index(): "service": "Windows AI Server (Refactored)" } + @app.get("/trade/balance") @app.get("/api/trade/balance") async def get_balance(): @@ -125,28 +143,29 @@ async def get_balance(): return {"error": "Server not initialized"} return kis_client.get_balance() + @app.post("/trade/order") @app.post("/api/trade/order") async def manual_order(req: ManualOrderRequest): ticker = req.ticker qty = req.quantity action = req.action.upper() - + result = "No Action" if action == "BUY": result = kis_client.buy_stock(ticker, qty) elif action == "SELL": result = kis_client.sell_stock(ticker, qty) - + return {"status": "executed", "kis_result": result} + @app.post("/analyze/portfolio") @app.post("/api/analyze/portfolio") async def analyze_portfolio(): - # ๊ฐ„๋‹จํ™”๋œ ๋ถ„์„ ๋กœ์ง balance = kis_client.get_balance() news = news_collector.get_market_news() - + prompt = f""" Analyze this portfolio with recent news: Portfolio: {balance} @@ -156,13 +175,13 @@ async def analyze_portfolio(): analysis = ai_agent.request_inference(prompt) return {"analysis": analysis} + if __name__ == "__main__": - # [์•ˆ์ •์„ฑ] ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ด์ „ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค ์ •๋ฆฌ + # ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค ์ •๋ฆฌ try: - from modules.utils.process_tracker import ProcessTracker ProcessTracker.check_and_kill_zombies() - except: pass - - # Reload=True๋Š” ๋ฉ€ํ‹ฐํ”„๋กœ์„ธ์‹ฑ ์ž์‹ ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ์— ์ทจ์•ฝํ•˜๋ฏ€๋กœ ๋น„ํ™œ์„ฑํ™” ๊ถŒ์žฅ - print("๐Ÿš€ Starting Windows AI Server...") + except Exception: + pass + + print("[Server] Starting Windows AI Server...") uvicorn.run("main_server:app", host="0.0.0.0", port=8000, reload=False) diff --git a/modules/analysis/deep_learning.py b/modules/analysis/deep_learning.py index 32e0248..c16c1d4 100644 --- a/modules/analysis/deep_learning.py +++ b/modules/analysis/deep_learning.py @@ -1,40 +1,36 @@ +import os +import time import torch import torch.nn as nn import numpy as np from sklearn.preprocessing import MinMaxScaler +from modules.config import Config + +# cuDNN ๋ฒค์น˜๋งˆํฌ ํ™œ์„ฑํ™” (๊ณ ์ • ์ž…๋ ฅ ํฌ๊ธฐ์— ๋Œ€ํ•ด ์ตœ์  ์ปค๋„ ์ž๋™ ์„ ํƒ) +torch.backends.cudnn.benchmark = True + + class Attention(nn.Module): - """Attention Mechanism for LSTM""" def __init__(self, hidden_size): super(Attention, self).__init__() - self.hidden_size = hidden_size self.attn = nn.Linear(hidden_size, 1) - + def forward(self, lstm_output): - # lstm_output: [batch_size, seq_len, hidden_size] - # attn_weights: [batch_size, seq_len, 1] attn_weights = torch.softmax(self.attn(lstm_output), dim=1) - # context: [batch_size, hidden_size] context = torch.sum(attn_weights * lstm_output, dim=1) return context, attn_weights + class AdvancedLSTM(nn.Module): - """ - [RTX 5070 Ti Optimized] High-Capacity LSTM with Attention - - Hidden Size: 512 (Rich Feature Extraction) - - Layers: 4 (Deep Reasoning) - - Attention: Focus on critical time steps - """ def __init__(self, input_size=1, hidden_size=512, num_layers=4, output_size=1, dropout=0.3): super(AdvancedLSTM, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers - - self.lstm = nn.LSTM(input_size, hidden_size, num_layers, - batch_first=True, dropout=dropout) - + + self.lstm = nn.LSTM(input_size, hidden_size, num_layers, + batch_first=True, dropout=dropout) self.attention = Attention(hidden_size) - self.fc = nn.Sequential( nn.Linear(hidden_size, hidden_size // 2), nn.ReLU(), @@ -45,162 +41,371 @@ class AdvancedLSTM(nn.Module): ) def forward(self, x): - # x: [batch, seq, feature] h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) - - # LSTM Output - lstm_out, _ = self.lstm(x, (h0, c0)) # [batch, seq, hidden] - - # Attention Mechanism - context, _ = self.attention(lstm_out) # [batch, hidden] - - # Final Prediction + lstm_out, _ = self.lstm(x, (h0, c0)) + context, _ = self.attention(lstm_out) out = self.fc(context) return out + +def _unload_ollama(): + """LSTM ํ•™์Šต ์ „ Ollama ๋ชจ๋ธ ์–ธ๋กœ๋“œํ•˜์—ฌ GPU ๋ฉ”๋ชจ๋ฆฌ ํ™•๋ณด""" + try: + import requests + url = f"{Config.OLLAMA_API_URL}/api/generate" + requests.post(url, json={ + "model": Config.OLLAMA_MODEL, + "keep_alive": 0 + }, timeout=5) + print("[AI] Ollama model unloaded (GPU memory freed)") + time.sleep(1) # ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ ๋Œ€๊ธฐ + except Exception: + pass + + +def _preload_ollama(): + """LSTM ํ•™์Šต ํ›„ Ollama ๋ชจ๋ธ ๋‹ค์‹œ ๋กœ๋“œ""" + try: + import requests + url = f"{Config.OLLAMA_API_URL}/api/generate" + requests.post(url, json={ + "model": Config.OLLAMA_MODEL, + "prompt": "", + "keep_alive": "10m" + }, timeout=10) + except Exception: + pass + + +def _log_gpu_memory(tag=""): + """GPU ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋กœ๊น…""" + if torch.cuda.is_available(): + allocated = torch.cuda.memory_allocated(0) / 1024**3 + reserved = torch.cuda.memory_reserved(0) / 1024**3 + print(f"[AI GPU {tag}] Allocated: {allocated:.2f}GB / Reserved: {reserved:.2f}GB") + + class PricePredictor: """ - ์ฃผ๊ฐ€ ์˜ˆ์ธก์„ ์œ„ํ•œ ๊ณ ์„ฑ๋Šฅ Deep Learning ๋ชจ๋ธ (RTX 5070 Ti Edition) + ์ฃผ๊ฐ€ ์˜ˆ์ธก Deep Learning ๋ชจ๋ธ (GPU ์ตœ์ ํ™”) + - ์ „์ฒด ํ•™์Šต ๋ฐ์ดํ„ฐ๋ฅผ GPU์— ์ƒ์ฃผ (CPUโ†”GPU ์ „์†ก ์ตœ์†Œํ™”) + - Ollama ๋ชจ๋ธ ์–ธ๋กœ๋“œ/๋ฆฌ๋กœ๋“œ๋กœ GPU ๋ฉ”๋ชจ๋ฆฌ ํ™•๋ณด + - Early Stopping + Mixed Precision (FP16) + - ์ข…๋ชฉ๋ณ„ ๋ชจ๋ธ ์ฒดํฌํฌ์ธํŠธ """ def __init__(self): self.scaler = MinMaxScaler(feature_range=(0, 1)) - - # [Hardware Spec] RTX 5070 Ti (16GB VRAM) ๋งž์ถค ์„ค์ • + self.hidden_size = 512 self.num_layers = 4 - - self.model = AdvancedLSTM(input_size=1, hidden_size=self.hidden_size, - num_layers=self.num_layers, dropout=0.3) + + self.model = AdvancedLSTM(input_size=1, hidden_size=self.hidden_size, + num_layers=self.num_layers, dropout=0.3) self.criterion = nn.MSELoss() - + # CUDA ์„ค์ • self.device = torch.device('cpu') - + self.use_amp = False + if torch.cuda.is_available(): try: gpu_name = torch.cuda.get_device_name(0) vram_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3 - - # GPU ํ• ๋‹น + self.device = torch.device('cuda') self.model.to(self.device) - - # Warm-up (์ปดํŒŒ์ผ ์ตœ์ ํ™” ์œ ๋„) - dummy = torch.zeros(1, 60, 1).to(self.device) - _ = self.model(dummy) - - print(f"๐Ÿš€ [AI] Powered by {gpu_name} ({vram_gb:.1f}GB) - High Performance Mode On") - + + # Mixed Precision (Compute Capability >= 7.0: Volta ์ด์ƒ) + if torch.cuda.get_device_capability(0)[0] >= 7: + self.use_amp = True + + # Warm-up: CUDA ์ปค๋„ ์ปดํŒŒ์ผ ์œ ๋„ + dummy = torch.zeros(1, 60, 1, device=self.device) + with torch.no_grad(): + _ = self.model(dummy) + torch.cuda.synchronize() + + print(f"[AI] GPU Mode: {gpu_name} ({vram_gb:.1f}GB)" + f" | FP16={'ON' if self.use_amp else 'OFF'}" + f" | cuDNN Benchmark=ON") + _log_gpu_memory("init") + except Exception as e: - print(f"โš ๏ธ [AI] GPU Init Failed: {e}") + print(f"[AI] GPU Init Failed ({e}), falling back to CPU") self.device = torch.device('cpu') + self.model.to(self.device) else: - print("โš ๏ธ [AI] Running on CPU (Low Performance)") - - # Optimizer ์„ค์ • (AdamW๊ฐ€ ์ผ๋ฐ˜ํ™” ์„ฑ๋Šฅ์ด ์ข€ ๋” ์ข‹์Œ) + print("[AI] No CUDA GPU detected. Running on CPU.") + self.optimizer = torch.optim.AdamW(self.model.parameters(), lr=0.0005, weight_decay=1e-4) - - # ํ•™์Šต ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ•ํ™” + self.scaler_amp = torch.amp.GradScaler('cuda') if self.use_amp else None + self.batch_size = 64 - self.epochs = 200 # ์ถฉ๋ถ„ํ•œ ํ•™์Šต - self.seq_length = 60 # 60์ผ(์•ฝ 3๊ฐœ์›”) ํŒจํ„ด ๋ถ„์„ - + self.max_epochs = 200 + self.seq_length = 60 + self.patience = 15 + self.training_status = { "is_training": False, - "loss": 0.0 + "loss": 0.0, + "current_ticker": None } @staticmethod def verify_hardware(): - """์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ํ•˜๋“œ์›จ์–ด ๊ฐ€์† ์—ฌ๋ถ€ ์ ๊ฒ€ ๋ฐ ๋กœ๊ทธ ์ถœ๋ ฅ""" if torch.cuda.is_available(): try: gpu_name = torch.cuda.get_device_name(0) vram_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3 - print(f"๐Ÿš€ [AI Check] Hardware Detected: {gpu_name} ({vram_gb:.1f}GB VRAM)") - print(f" โœ… High Performance Mode is READY.") + print(f"[AI Check] {gpu_name} ({vram_gb:.1f}GB VRAM) | cuDNN={torch.backends.cudnn.is_available()}") return True except Exception as e: - print(f"โš ๏ธ [AI Check] GPU Error: {e}") + print(f"[AI Check] GPU Error: {e}") return False - else: - print("โš ๏ธ [AI Check] No GPU Detected. Running in CPU Mode.") - return False + print("[AI Check] No GPU. CPU Mode.") + return False - def train_and_predict(self, prices, forecast_days=1): - """ - Online Learning & Prediction - """ - # ๋ฐ์ดํ„ฐ๊ฐ€ ์ตœ์†Œ ์‹œํ€€์Šค ๊ธธ์ด + ์—ฌ์œ ๋ถ„๋ณด๋‹ค ์ ์œผ๋ฉด ์˜ˆ์ธก ๋ถˆ๊ฐ€ + def _get_checkpoint_path(self, ticker): + return os.path.join(Config.MODEL_DIR, f"{ticker}_lstm.pt") + + def _load_checkpoint(self, ticker): + path = self._get_checkpoint_path(ticker) + if os.path.exists(path): + try: + checkpoint = torch.load(path, map_location=self.device, weights_only=True) + self.model.load_state_dict(checkpoint['model_state_dict']) + self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + print(f"[AI] Checkpoint loaded: {ticker}") + return True + except Exception as e: + print(f"[AI] Checkpoint load failed ({ticker}): {e}") + return False + + def _save_checkpoint(self, ticker, epoch, loss): + path = self._get_checkpoint_path(ticker) + try: + torch.save({ + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + 'epoch': epoch, + 'loss': loss + }, path) + except Exception as e: + print(f"[AI] Checkpoint save failed ({ticker}): {e}") + + def train_and_predict(self, prices, forecast_days=1, ticker=None): if len(prices) < (self.seq_length + 10): - return None - - # 1. ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ + return None + + is_gpu = self.device.type == 'cuda' + + # --- Ollama ๋ชจ๋ธ ์–ธ๋กœ๋“œ (GPU ๋ฉ”๋ชจ๋ฆฌ ํ™•๋ณด) --- + if is_gpu: + _unload_ollama() + torch.cuda.empty_cache() + _log_gpu_memory("pre-train") + + t_start = time.time() + + # 1. ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ (CPU์—์„œ numpy ์ž‘์—…) data = np.array(prices).reshape(-1, 1) scaled_data = self.scaler.fit_transform(data) - - x_train, y_train = [], [] + + x_seqs, y_seqs = [], [] for i in range(len(scaled_data) - self.seq_length): - x_train.append(scaled_data[i:i+self.seq_length]) - y_train.append(scaled_data[i+self.seq_length]) - - x_train_t = torch.FloatTensor(np.array(x_train)).to(self.device) - y_train_t = torch.FloatTensor(np.array(y_train)).to(self.device) - - # 2. ํ•™์Šต + x_seqs.append(scaled_data[i:i + self.seq_length]) + y_seqs.append(scaled_data[i + self.seq_length]) + + # 2. ํ…์„œ ์ƒ์„ฑ โ†’ ์ฆ‰์‹œ GPU๋กœ ์ด๋™ (์ดํ›„ CPUโ†”GPU ์ „์†ก ์—†์Œ) + x_all = torch.FloatTensor(np.array(x_seqs)).to(self.device) + y_all = torch.FloatTensor(np.array(y_seqs)).to(self.device) + + # Validation split (80/20) + split_idx = int(len(x_all) * 0.8) + x_train = x_all[:split_idx] + y_train = y_all[:split_idx] + x_val = x_all[split_idx:] + y_val = y_all[split_idx:] + + dataset_size = len(x_train) + + # 3. ์ฒดํฌํฌ์ธํŠธ ๋กœ๋“œ + has_checkpoint = False + if ticker: + has_checkpoint = self._load_checkpoint(ticker) + max_epochs = 50 if has_checkpoint else self.max_epochs + + # 4. ํ•™์Šต (์ „์ฒด ๋ฐ์ดํ„ฐ GPU ์ƒ์ฃผ, DataLoader ๋ฏธ์‚ฌ์šฉ) self.model.train() self.training_status["is_training"] = True - - dataset_size = len(x_train_t) + if ticker: + self.training_status["current_ticker"] = ticker + + best_val_loss = float('inf') + patience_counter = 0 final_loss = 0.0 - - for epoch in range(self.epochs): - perm = torch.randperm(dataset_size).to(self.device) - x_shuffled = x_train_t[perm] - y_shuffled = y_train_t[perm] - + actual_epochs = 0 + + for epoch in range(max_epochs): + # --- Training (GPU ๋‚ด์—์„œ ์…”ํ”Œ + ๋ฏธ๋‹ˆ๋ฐฐ์น˜) --- + perm = torch.randperm(dataset_size, device=self.device) + x_shuffled = x_train[perm] + y_shuffled = y_train[perm] + epoch_loss = 0.0 steps = 0 - + for i in range(0, dataset_size, self.batch_size): - batch_x = x_shuffled[i:min(i+self.batch_size, dataset_size)] - batch_y = y_shuffled[i:min(i+self.batch_size, dataset_size)] - - self.optimizer.zero_grad() - outputs = self.model(batch_x) - loss = self.criterion(outputs, batch_y) - loss.backward() - self.optimizer.step() - + end = min(i + self.batch_size, dataset_size) + batch_x = x_shuffled[i:end] + batch_y = y_shuffled[i:end] + + self.optimizer.zero_grad(set_to_none=True) + + if self.use_amp: + with torch.amp.autocast('cuda'): + outputs = self.model(batch_x) + loss = self.criterion(outputs, batch_y) + self.scaler_amp.scale(loss).backward() + self.scaler_amp.step(self.optimizer) + self.scaler_amp.update() + else: + outputs = self.model(batch_x) + loss = self.criterion(outputs, batch_y) + loss.backward() + self.optimizer.step() + epoch_loss += loss.item() steps += 1 - - final_loss = epoch_loss / max(1, steps) - + + train_loss = epoch_loss / max(1, steps) + + # --- Validation (GPU์—์„œ ์ง์ ‘ ์ˆ˜ํ–‰) --- + self.model.eval() + with torch.no_grad(): + if self.use_amp: + with torch.amp.autocast('cuda'): + val_out = self.model(x_val) + val_loss = self.criterion(val_out, y_val).item() + else: + val_out = self.model(x_val) + val_loss = self.criterion(val_out, y_val).item() + self.model.train() + + final_loss = train_loss + actual_epochs = epoch + 1 + + if val_loss < best_val_loss: + best_val_loss = val_loss + patience_counter = 0 + else: + patience_counter += 1 + if patience_counter >= self.patience: + break + self.training_status["is_training"] = False self.training_status["loss"] = final_loss - - # 3. ์˜ˆ์ธก + + if is_gpu: + torch.cuda.synchronize() + + elapsed = time.time() - t_start + print(f"[AI] {ticker or '?'}: {actual_epochs} epochs in {elapsed:.1f}s" + f" | loss={final_loss:.6f} val={best_val_loss:.6f}" + f" | device={self.device}") + + # 5. ์ฒดํฌํฌ์ธํŠธ ์ €์žฅ + if ticker: + self._save_checkpoint(ticker, actual_epochs, final_loss) + + # 6. ์˜ˆ์ธก self.model.eval() with torch.no_grad(): - last_seq = torch.FloatTensor(scaled_data[-self.seq_length:]).unsqueeze(0).to(self.device) - predicted_scaled = self.model(last_seq) - predicted_price = self.scaler.inverse_transform(predicted_scaled.cpu().numpy())[0][0] - + last_seq = torch.FloatTensor( + scaled_data[-self.seq_length:] + ).unsqueeze(0).to(self.device) + + if self.use_amp: + with torch.amp.autocast('cuda'): + predicted_scaled = self.model(last_seq) + else: + predicted_scaled = self.model(last_seq) + + predicted_price = self.scaler.inverse_transform( + predicted_scaled.cpu().float().numpy())[0][0] + + # 7. GPU ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ + Ollama ๋ฆฌ๋กœ๋“œ + if is_gpu: + # ํ•™์Šต ์ค‘๊ฐ„ ํ…์„œ ํ•ด์ œ + del x_all, y_all, x_train, y_train, x_val, y_val + torch.cuda.empty_cache() + _log_gpu_memory("post-train") + _preload_ollama() + current_price = prices[-1] trend = "UP" if predicted_price > current_price else "DOWN" change_rate = ((predicted_price - current_price) / current_price) * 100 - - # ์‹ ๋ขฐ๋„ ์ ์ˆ˜ (Loss๊ฐ€ ๋‚ฎ์„์ˆ˜๋ก ๋†’์Œ, 0~1) - # Loss๊ฐ€ 0.001์ด๋ฉด 0.99, 0.01์ด๋ฉด 0.9 ์ •๋„ ๋‚˜์˜ค๊ฒŒ ์กฐ์ • confidence = 1.0 / (1.0 + (final_loss * 100)) - + return { "current": current_price, "predicted": float(predicted_price), "change_rate": round(change_rate, 2), "trend": trend, "loss": final_loss, - "confidence": round(confidence, 2) + "confidence": round(confidence, 2), + "epochs": actual_epochs, + "device": str(self.device) } + + def batch_predict(self, prices_dict): + results = {} + seqs = [] + metas = [] + + for ticker, prices in prices_dict.items(): + if len(prices) < (self.seq_length + 10): + results[ticker] = None + continue + + data = np.array(prices).reshape(-1, 1) + scaler = MinMaxScaler(feature_range=(0, 1)) + scaled_data = scaler.fit_transform(data) + + seq = torch.FloatTensor(scaled_data[-self.seq_length:]).unsqueeze(0) + seqs.append(seq) + metas.append((ticker, scaler, prices[-1])) + + if not seqs: + return results + + # ๋ฐฐ์น˜๋กœ ํ•ฉ์ณ์„œ ํ•œ๋ฒˆ์— GPU ์ถ”๋ก  + batch = torch.cat(seqs, dim=0).to(self.device) + + self.model.eval() + with torch.no_grad(): + if self.use_amp: + with torch.amp.autocast('cuda'): + preds = self.model(batch) + else: + preds = self.model(batch) + + preds_cpu = preds.cpu().float().numpy() + + for i, (ticker, scaler, current_price) in enumerate(metas): + predicted_price = scaler.inverse_transform(preds_cpu[i:i+1])[0][0] + trend = "UP" if predicted_price > current_price else "DOWN" + change_rate = ((predicted_price - current_price) / current_price) * 100 + + results[ticker] = { + "current": current_price, + "predicted": float(predicted_price), + "change_rate": round(change_rate, 2), + "trend": trend + } + + if self.device.type == 'cuda': + torch.cuda.empty_cache() + + return results diff --git a/modules/bot.py b/modules/bot.py index e5aa4b9..cd94dee 100644 --- a/modules/bot.py +++ b/modules/bot.py @@ -1,172 +1,203 @@ -import time +import asyncio import os -import sys import json +import time from concurrent.futures import ProcessPoolExecutor from concurrent.futures.process import BrokenProcessPool from datetime import datetime -# ๋ชจ๋“ˆ ์ž„ํฌํŠธ from modules.config import Config from modules.services.kis import KISClient -from modules.services.news import NewsCollector +from modules.services.news import AsyncNewsCollector from modules.services.ollama import OllamaManager from modules.services.telegram import TelegramMessenger from modules.analysis.macro import MacroAnalyzer from modules.utils.monitor import SystemMonitor from modules.strategy.process import analyze_stock_process -# ๊ธฐ์กด ์ฝ”๋“œ์™€์˜ ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•ด ์ƒ๋Œ€ ๊ฒฝ๋กœ๋‚˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ์ž„ํฌํŠธ -# (๋ฆฌํŒฉํ† ๋ง ๊ณผ๋„๊ธฐ์—๋Š” ์ผ๋ถ€ ํŒŒ์ผ์€ ๊ทธ๋Œ€๋กœ ์žˆ์„ ์ˆ˜ ์žˆ์Œ) try: from theme_manager import ThemeManager except ImportError: - # ํ…œํ”Œ๋ฆฟ์šฉ ๋”๋ฏธ class ThemeManager: def get_themes(self, code): return [] -# ์›Œ์ปค ํ”„๋กœ์„ธ์Šค ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ + def init_worker(): try: from modules.utils.process_tracker import ProcessTracker ProcessTracker.register("Trading Bot Worker") - except: pass + except Exception: + pass + class AutoTradingBot: - def __init__(self): + def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None): # 1. ์„œ๋น„์Šค ์ดˆ๊ธฐํ™” self.kis = KISClient() - self.news = NewsCollector() - # [์•ˆ์ •์„ฑ] GPU OOM ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์›Œ์ปค ์ˆ˜ ์ถ•์†Œ (4 -> 2) - # [์‹๋ณ„] ์›Œ์ปค ํ”„๋กœ์„ธ์Šค ์ด๋ฆ„ ๋“ฑ๋ก - self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker) - # ์›Œ์ปค ํ”„๋กœ์„ธ์Šค ์›Œ๋ฐ์—… (PID ๋“ฑ๋ก ์œ ๋„) + self.news = AsyncNewsCollector() + # GPU ๊ฒฝํ•ฉ ๋ฐฉ์ง€: ์›Œ์ปค 1๊ฐœ๋งŒ ์‚ฌ์šฉ (LSTM ํ•™์Šต์ด GPU ๋…์ ) + self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker) try: - list(self.executor.map(lambda x: x, range(2))) - except: pass - - self.messenger = TelegramMessenger() + list(self.executor.map(lambda x: x, range(1))) + except Exception: + pass + + self.messenger = TelegramMessenger() self.theme_manager = ThemeManager() - self.ollama_monitor = OllamaManager() # GPU ๋ชจ๋‹ˆํ„ฐ๋ง์šฉ - + self.ollama_monitor = OllamaManager() + # 2. ์œ ํ‹ธ๋ฆฌํ‹ฐ ์ดˆ๊ธฐํ™” self.monitor = SystemMonitor(self.messenger, self.ollama_monitor) - + # 3. ์ƒํƒœ ๋ณ€์ˆ˜ - self.daily_trade_history = [] + self.daily_trade_history = [] self.discovered_stocks = set() self.is_macro_warning_sent = False self.watchlist_updated_today = False self.report_sent = False - - # [IPC] BotIPC + + # 4. ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ + self.shutdown_event = shutdown_event + + # 5. IPC (Shared Memory) try: - from modules.utils.ipc import BotIPC - self.ipc = BotIPC() + from modules.utils.ipc import SharedIPC + self.ipc = SharedIPC(lock=ipc_lock, command_queue=command_queue) except ImportError: - print("โš ๏ธ BotIPC module not found.") + print("[Bot] SharedIPC module not found.") self.ipc = None - # [Watchlist Manager] + # 6. Watchlist Manager try: from watchlist_manager import WatchlistManager self.watchlist_manager = WatchlistManager(self.kis, watchlist_file=Config.WATCHLIST_FILE) except ImportError: self.watchlist_manager = None - # ๊ธฐ๋ก ๋กœ๋“œ + # 7. ๊ธฐ๋ก ๋กœ๋“œ self.history_file = Config.HISTORY_FILE self.load_trade_history() - - # AI ๋ฐ ํ•˜๋“œ์›จ์–ด ์ ๊ฒ€ + + # 8. AI ํ•˜๋“œ์›จ์–ด ์ ๊ฒ€ from modules.analysis.deep_learning import PricePredictor PricePredictor.verify_hardware() - - pass + + # 9. KIS ๋น„๋™๊ธฐ ํด๋ผ์ด์–ธํŠธ + try: + from modules.services.kis import KISAsyncClient + self.kis_async = KISAsyncClient(self.kis) + except ImportError: + self.kis_async = None def load_trade_history(self): - """ํŒŒ์ผ์—์„œ ๊ธˆ์ผ ๋งค๋งค ๊ธฐ๋ก ๋กœ๋“œ""" if os.path.exists(self.history_file): try: with open(self.history_file, "r", encoding="utf-8") as f: self.daily_trade_history = json.load(f) - except Exception as e: - print(f"โš ๏ธ Failed to load history: {e}") + except Exception: self.daily_trade_history = [] else: self.daily_trade_history = [] def save_trade_history(self): - """๋งค๋งค ๊ธฐ๋ก ์ €์žฅ""" try: with open(self.history_file, "w", encoding="utf-8") as f: json.dump(self.daily_trade_history, f, ensure_ascii=False, indent=2) except Exception as e: - print(f"โš ๏ธ Failed to save history: {e}") + print(f"[Bot] Failed to save history: {e}") def load_watchlist(self): try: with open(Config.WATCHLIST_FILE, "r", encoding="utf-8") as f: return json.load(f) - except: + except Exception: return {} def send_daily_report(self): - """์žฅ ๋งˆ๊ฐ ๋ฆฌํฌํŠธ""" - if self.report_sent: return - - print("๐Ÿ“ [Bot] Generating Daily Report...") + if self.report_sent: + return + print("[Bot] Generating Daily Report...") balance = self.kis.get_balance() - - total_eval = 0 - if "total_eval" in balance: - total_eval = int(balance.get("total_eval", 0)) - report = f"๐Ÿ“… **[Daily Closing Report]**\n" \ - f"โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n" \ - f"๐Ÿ’ฐ **์ด ์ž์‚ฐ**: `{total_eval:,}์›`\n" \ - f"๐Ÿ“œ **์˜ค๋Š˜์˜ ๋งค๋งค**: `{len(self.daily_trade_history)}๊ฑด`\n\n" + total_eval = int(balance.get("total_eval", 0)) + report = f"๐Ÿ“… [Daily Closing Report]\n" \ + f"๐Ÿ’ฐ Total Asset: {total_eval:,}์›\n" \ + f"๐Ÿ“œ Trades Today: {len(self.daily_trade_history)}๊ฑด\n\n" if self.daily_trade_history: for trade in self.daily_trade_history: - icon = "๐Ÿ”ด" if trade['action'] == "BUY" else "๐Ÿ”ต" - report += f"{icon} {trade['name']} {trade['qty']}์ฃผ\n" - + action = trade['action'] + icon = "๐Ÿ”ด" if action == "BUY" else "๐Ÿ”ต" + report += f"{icon} {action} {trade['name']} {trade['qty']}์ฃผ\n" + if "holdings" in balance and balance["holdings"]: - report += "\n๐Ÿ“Š **๋ณด์œ  ํ˜„ํ™ฉ**\n" + report += "\n๐Ÿ“Š [Holdings]\n" for stock in balance["holdings"]: - yld = stock.get('yield', 0) - icon = "๐Ÿ”บ" if yld > 0 else "๐Ÿ”ป" - report += f"{icon} {stock['name']}: `{yld}%`\n" + yld = float(stock.get('yield', 0)) + if yld > 0: + icon = "๐Ÿ”ด" + yld_str = f"+{yld}" + elif yld < 0: + icon = "๐Ÿ”ต" + yld_str = f"{yld}" + else: + icon = "โšช" + yld_str = f"{yld}" + + report += f"{icon} {stock['name']}: {yld_str}%\n" self.messenger.send_message(report) self.report_sent = True def restart_executor(self): - """ํ”„๋กœ์„ธ์Šค ํ’€ ๋ณต๊ตฌ""" - print("๐Ÿ”„ [Bot] Restarting Process Executor...") + print("[Bot] Restarting Process Executor...") try: self.executor.shutdown(wait=False) - except: + except Exception: pass - # ์›Œ์ปค ์žฌ์‹œ์ž‘ - self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker) - print("โœ… [Bot] Process Executor Restarted.") + self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker) + print("[Bot] Process Executor Restarted.") - def run_cycle(self): + def _process_commands(self): + """IPC command queue ํด๋ง ๋ฐ ์ฒ˜๋ฆฌ""" + if not self.ipc: + return + + commands = self.ipc.poll_commands() + for cmd in commands: + command = cmd.get('command', '') + print(f"[Bot] Received command: {command}") + + if command == 'restart': + self.messenger.send_message("[Bot] Restart requested via Telegram.") + # executor ์žฌ์‹œ์ž‘ + self.restart_executor() + + elif command == 'update_watchlist': + if self.watchlist_manager: + try: + summary = self.watchlist_manager.update_watchlist_daily() + self.messenger.send_message(f"[Watchlist Updated]\n{summary}") + except Exception as e: + self.messenger.send_message(f"Watchlist update failed: {e}") + + async def run_cycle(self): now = datetime.now() - - # 1. ๊ฑฐ์‹œ๊ฒฝ์ œ ๋ถ„์„ (์šฐ์„  ์ˆœ์œ„ ์ƒํ–ฅ) + + # 0. ๋ช…๋ น ํ ํด๋ง + self._process_commands() + + # 1. ๊ฑฐ์‹œ๊ฒฝ์ œ ๋ถ„์„ macro_status = MacroAnalyzer.get_macro_status(self.kis) is_crash = False if macro_status['status'] == 'DANGER': is_crash = True if not self.is_macro_warning_sent: - self.messenger.send_message("๐Ÿšจ **[MARKET CRASH ALERT]** ์‹œ์žฅ ๊ธ‰๋ฝ ๊ฐ์ง€! ๋งค์ˆ˜ ์ค‘๋‹จ.") + self.messenger.send_message("๐Ÿšจ [MARKET CRASH ALERT] ์‹œ์žฅ ๊ธ‰๋ฝ ๊ฐ์ง€! ๋งค์ˆ˜ ์ค‘๋‹จ.") self.is_macro_warning_sent = True else: if self.is_macro_warning_sent: - self.messenger.send_message("๐ŸŒค๏ธ **[MARKET RECOVERY]** ์‹œ์žฅ ์•ˆ์ •ํ™”.") + self.messenger.send_message("๐ŸŒค๏ธ [MARKET RECOVERY] ์‹œ์žฅ ์•ˆ์ •ํ™”.") self.is_macro_warning_sent = False # 2. IPC ์ƒํƒœ ์—…๋ฐ์ดํŠธ @@ -181,16 +212,16 @@ class AutoTradingBot: 'watchlist': watchlist, 'discovered_stocks': list(self.discovered_stocks), 'is_macro_warning': self.is_macro_warning_sent, - 'macro_indices': macro_status['indicators'], # [์ˆ˜์ •] ๊ฑฐ์‹œ๊ฒฝ์ œ ์ง€ํ‘œ ์ „๋‹ฌ + 'macro_indices': macro_status['indicators'], 'themes': {} }) except Exception: pass - # 2. ์•„์นจ ์—…๋ฐ์ดํŠธ (08:00) + # 3. ์•„์นจ ์—…๋ฐ์ดํŠธ (08:00) if now.hour == 8 and 0 <= now.minute < 5: if not self.watchlist_updated_today and self.watchlist_manager: - print("๐ŸŒ… Morning Update...") + print("[Bot] Morning Update...") try: summary = self.watchlist_manager.update_watchlist_daily() self.messenger.send_message(summary) @@ -198,7 +229,7 @@ class AutoTradingBot: except Exception as e: self.messenger.send_message(f"Update Failed: {e}") - # 3. ๋ฆฌ์…‹ (09:00) + # 4. ๋ฆฌ์…‹ (09:00) if now.hour == 9 and now.minute < 5: self.daily_trade_history = [] self.save_trade_history() @@ -206,182 +237,168 @@ class AutoTradingBot: self.discovered_stocks.clear() self.watchlist_updated_today = False - # 4. ์‹œ์Šคํ…œ ๊ฐ์‹œ + # 5. ์‹œ์Šคํ…œ ๊ฐ์‹œ (3๋ถ„ ๊ฐ„๊ฒฉ) self.monitor.check_health() - # 5. ์žฅ ์šด์˜ ์‹œ๊ฐ„ ์ฒดํฌ + # 6. ์žฅ ์šด์˜ ์‹œ๊ฐ„ ์ฒดํฌ if not (9 <= now.hour < 15 or (now.hour == 15 and now.minute < 30)): - # ์žฅ ๋งˆ๊ฐ ๋ฆฌํฌํŠธ (15:40) if now.hour == 15 and now.minute >= 40: self.send_daily_report() - - print("๐Ÿ’ค Market Closed. Waiting...") + print("[Bot] Market Closed. Waiting...") return - print(f"โฐ [Bot] Cycle Start: {now.strftime('%H:%M:%S')}") - - # 6. ๊ฑฐ์‹œ๊ฒฝ์ œ ๋ถ„์„ (์™„๋ฃŒ๋จ) - # macro_status = ... (Moved to top) + print(f"[Bot] Cycle Start: {now.strftime('%H:%M:%S')}") # 7. ์ข…๋ชฉ ๋ถ„์„ ๋ฐ ๋งค๋งค target_dict = self.load_watchlist() - # (Discovery ๋กœ์ง ์ƒ๋žต - ํ•„์š”์‹œ ์ถ”๊ฐ€) - + # ๋ณด์œ  ์ข…๋ชฉ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ balance = self.kis.get_balance() current_holdings = {} - + if balance and "holdings" in balance: for stock in balance["holdings"]: code = stock.get("code") name = stock.get("name") qty = int(stock.get("qty", 0)) yld = float(stock.get("yield", 0.0)) - + current_holdings[code] = stock - - # ๋ณด์œ  ์ˆ˜๋Ÿ‰์ด 0 ์ดํ•˜๋ผ๋ฉด ์Šคํ‚ต (์˜ค๋ฅ˜ ๋ฐฉ์ง€) + if qty <= 0: continue - + action = None reason = "" - - # ์†์ ˆ/์ต์ ˆ ๋กœ์ง - if yld <= -5.0: + + if yld <= -5.0: action = "SELL" - reason = "Stop Loss ๐Ÿ“‰" - elif yld >= 8.0: + reason = "Stop Loss" + elif yld >= 8.0: action = "SELL" - reason = "Take Profit ๐Ÿš€" - + reason = "Take Profit" + if action == "SELL": - print(f"๐Ÿšจ Risk Management: {reason} - {name} (Qty: {qty}, Yield: {yld}%)") - - # ์ „๋Ÿ‰ ๋งค๋„ + print(f"[Bot] Risk Management: {reason} - {name} (Qty: {qty}, Yield: {yld}%)") res = self.kis.sell_stock(code, qty) - if res and res.get("status"): - self.messenger.send_message(f"๐Ÿ›ก๏ธ **[Risk SELL]** {name}\n" - f" โ€ข ์‚ฌ์œ : {reason}\n" - f" โ€ข ์ˆ˜๋Ÿ‰: {qty}์ฃผ\n" - f" โ€ข ์ˆ˜์ต๋ฅ : {yld}%") - + self.messenger.send_message( + f"๐Ÿ”ต [Risk SELL] {name}\n" + f" Reason: {reason}\n" + f" Qty: {qty}\n" + f" Yield: {yld}%") self.daily_trade_history.append({ - "action": "SELL", - "name": name, - "qty": qty, - "price": stock.get('current_price'), - "yield": yld + "action": "SELL", "name": name, "qty": qty, + "price": stock.get('current_price'), "yield": yld }) self.save_trade_history() - else: - print(f"โŒ Sell Failed for {name}: {res}") # ๋ถ„์„ ์‹คํ–‰ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) analysis_tasks = [] - news_data = self.news.get_market_news() - - # [์ˆ˜์ •] ์‹ค์‹œ๊ฐ„ ์ž”๊ณ  ์ถ”์ ์šฉ ๋ณ€์ˆ˜ (๋งค์ˆ˜ ์‹œ ์ฐจ๊ฐ) + news_data = await self.news.get_market_news_async() + + # ์‹ค์‹œ๊ฐ„ ์ž”๊ณ  ์ถ”์ ์šฉ ๋ณ€์ˆ˜ (๋งค์ˆ˜ ์‹œ ์ฐจ๊ฐ) tracking_deposit = int(balance.get("deposit", 0)) - + try: for ticker, name in target_dict.items(): prices = self.kis.get_daily_price(ticker) - if not prices: continue - - # [์‹ ๊ทœ] ์™ธ์ธ ์ˆ˜๊ธ‰ ๋ถ„์„ + if not prices: + continue + + # ์™ธ์ธ ์ˆ˜๊ธ‰ ๋ถ„์„ investor_trend = self.kis.get_investor_trend(ticker) - - future = self.executor.submit(analyze_stock_process, ticker, prices, news_data, investor_trend) + + future = self.executor.submit( + analyze_stock_process, ticker, prices, news_data, investor_trend) analysis_tasks.append(future) - + # ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ + loop = asyncio.get_event_loop() for future in analysis_tasks: try: - res = future.result() + res = await loop.run_in_executor(None, future.result) ticker_name = target_dict.get(res['ticker'], 'Unknown') - print(f"๐Ÿ“Š [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})") - + print(f"[Bot] [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})") + if res['decision'] == "BUY": - if is_crash: continue - - # ๋งค์ˆ˜ ๋กœ์ง (์˜ˆ์ˆ˜๊ธˆ ์ฒดํฌ ์ถ”๊ฐ€) - current_qty = 0 - if res['ticker'] in current_holdings: - current_qty = current_holdings[res['ticker']]['qty'] - - current_price = float(res['current_price']) - if current_price <= 0: continue - - # ๋งค์ˆ˜ ์ˆ˜๋Ÿ‰ ๊ฒฐ์ • (๊ธฐ๋ณธ 1์ฃผ, ์ถ”ํ›„ ๊ธˆ์•ก ๊ธฐ๋ฐ˜์œผ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ) - qty = 1 - required_amount = current_price * qty - - # ์˜ˆ์ˆ˜๊ธˆ ํ™•์ธ - if tracking_deposit < required_amount: - print(f"๐Ÿ’ฐ [Skip Buy] ์˜ˆ์ˆ˜๊ธˆ ๋ถ€์กฑ ({ticker_name}): ํ•„์š” {required_amount:,.0f} > ์ž”๊ณ  {tracking_deposit:,.0f}") + if is_crash: continue - print(f"๐Ÿš€ Buying {ticker_name} {qty}ea") - - # ์‹ค์ œ ์ฃผ๋ฌธ + current_price = float(res['current_price']) + if current_price <= 0: + continue + + qty = 1 + required_amount = current_price * qty + + # ์˜ˆ์ˆ˜๊ธˆ ํ™•์ธ + if tracking_deposit < required_amount: + print(f"[Bot] [Skip Buy] ์˜ˆ์ˆ˜๊ธˆ ๋ถ€์กฑ ({ticker_name}): " + f"ํ•„์š” {required_amount:,.0f} > ์ž”๊ณ  {tracking_deposit:,.0f}") + continue + + print(f"[Bot] Buying {ticker_name} {qty}ea") order = self.kis.buy_stock(res['ticker'], qty) if order.get("status"): - self.messenger.send_message(f"๐Ÿš€ **[BUY]** {ticker_name} {qty}์ฃผ\n" - f" โ€ข ๊ฐ€๊ฒฉ: {current_price:,.0f}์›") - + self.messenger.send_message( + f"๐Ÿ”ด [BUY] {ticker_name} {qty}์ฃผ\n" + f" Price: {current_price:,.0f}์›") self.daily_trade_history.append({ - "action": "BUY", - "name": ticker_name, - "qty": qty, - "price": current_price + "action": "BUY", "name": ticker_name, + "qty": qty, "price": current_price }) self.save_trade_history() - - # [์ค‘์š”] ๊ฐ€์ƒ ์ž”๊ณ  ์ฐจ๊ฐ (์—ฐ์† ๋งค์ˆ˜ ์‹œ ์ดˆ๊ณผ ๋ฐฉ์ง€) tracking_deposit -= required_amount - + elif res['decision'] == "SELL": - print(f"๐Ÿ“‰ Selling {ticker_name} (Simulation)") - # ๋งค๋„ ๋กœ์ง (ํ•„์š” ์‹œ ์ถ”๊ฐ€) + print(f"[Bot] Selling {ticker_name} (Simulation)") + except BrokenProcessPool: - raise # ์ƒ์œ„ ๋ ˆ๋ฒจ์—์„œ ์ฒ˜๋ฆฌ + raise except Exception as e: - print(f"โŒ Analysis Worker Error: {e}") + print(f"[Bot] Analysis Worker Error: {e}") except BrokenProcessPool: - print("โš ๏ธ [Bot] Worker Process Crashed (OOM, CUDA Error?). Restarting Executor...") + print("[Bot] Worker Process Crashed. Restarting Executor...") self.restart_executor() except KeyboardInterrupt: raise except Exception as e: - print(f"โŒ Cycle Loop Error: {e}") + print(f"[Bot] Cycle Loop Error: {e}") def loop(self): - print(f"๐Ÿค– Bot Module Started (PID: {os.getpid()})") - self.messenger.send_message("๐Ÿค– **[Bot Started]** ๋ฆฌํŒฉํ† ๋ง๋œ ๋ด‡์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + print(f"[Bot] Module Started (PID: {os.getpid()})") + self.messenger.send_message("[Bot Started] ๋ฆฌํŒฉํ† ๋ง๋œ ๋ด‡์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: while True: - try: - self.run_cycle() - except Exception as e: - print(f"โš ๏ธ Loop Error: {e}") - self.messenger.send_message(f"โš ๏ธ Loop Error: {e}") - time.sleep(60) - except KeyboardInterrupt: - print("๐Ÿ›‘ [Bot] Stopped by User.") - finally: - print("๐Ÿ›‘ [Bot] Shutting down executor...") - self.executor.shutdown(wait=False) - print("โœ… [Bot] Executor shutdown complete.") + # shutdown ์‹œ๊ทธ๋„ ์ฒดํฌ + if self.shutdown_event and self.shutdown_event.is_set(): + print("[Bot] Shutdown signal received.") + break - def start_telegram_command_server(self): - """ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ ํ”„๋กœ์„ธ์Šค ์‹คํ–‰ (๋…๋ฆฝ ํ”„๋กœ์„ธ์Šค)""" - script = os.path.join(os.getcwd(), "modules", "services", "telegram_bot", "runner.py") - if os.path.exists(script): - import subprocess - subprocess.Popen([sys.executable, script], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) - print("๐Ÿš€ Telegram Command Server Started") - else: - print(f"โš ๏ธ Telegram Bot Runner not found: {script}") + try: + loop.run_until_complete(self.run_cycle()) + except Exception as e: + print(f"[Bot] Loop Error: {e}") + self.messenger.send_message(f"[Bot] Loop Error: {e}") + + # ๋น„๋™๊ธฐ sleep (shutdown ์ฒดํฌํ•˜๋ฉด์„œ ๋Œ€๊ธฐ) + for _ in range(60): + if self.shutdown_event and self.shutdown_event.is_set(): + break + time.sleep(1) + + except KeyboardInterrupt: + print("[Bot] Stopped by User.") + finally: + print("[Bot] Shutting down executor...") + self.executor.shutdown(wait=False) + if self.ipc: + self.ipc.cleanup() + loop.close() + print("[Bot] Executor shutdown complete.") diff --git a/modules/config.py b/modules/config.py index a0bf427..103307f 100644 --- a/modules/config.py +++ b/modules/config.py @@ -41,12 +41,28 @@ class Config: DATA_DIR = os.path.join(BASE_DIR, "data") if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR, exist_ok=True) - - IPC_FILE = os.path.join(DATA_DIR, "bot_ipc.json") + HISTORY_FILE = os.path.join(DATA_DIR, "daily_trade_history.json") WATCHLIST_FILE = os.path.join(DATA_DIR, "watchlist.json") - # 7. ํƒ€์ž„์•„์›ƒ ๋“ฑ + # ๋ชจ๋ธ ์ฒดํฌํฌ์ธํŠธ ๋””๋ ‰ํ† ๋ฆฌ + MODEL_DIR = os.path.join(DATA_DIR, "models") + if not os.path.exists(MODEL_DIR): + os.makedirs(MODEL_DIR, exist_ok=True) + + # 7. IPC ์„ค์ • + SHM_NAME = "web_ai_bot_ipc" + SHM_SIZE = 131072 # 128KB + IPC_STALENESS = 120 # 120์ดˆ (๋ฉ”์ธ ๋ด‡ ์‚ฌ์ดํด 60์ดˆ + ์—ฌ์œ ) + + # 8. GPU ์„ค์ • + VRAM_WARNING_THRESHOLD = 12.0 # GB (14 โ†’ 12๋กœ ์กฐ๊ธฐ ๊ฒฝ๊ณ ) + + # 9. ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ + WATCHDOG_INTERVAL = 30 # ํ—ฌ์Šค์ฒดํฌ ๊ฐ„๊ฒฉ(์ดˆ) + MAX_RESTART_COUNT = 3 # ์ตœ๋Œ€ ์ž๋™ ์žฌ์‹œ์ž‘ ํšŸ์ˆ˜ + + # 10. ํƒ€์ž„์•„์›ƒ ๋“ฑ HTTP_TIMEOUT = 10 @staticmethod diff --git a/modules/services/kis.py b/modules/services/kis.py index a79cf25..05fdfac 100644 --- a/modules/services/kis.py +++ b/modules/services/kis.py @@ -484,31 +484,174 @@ class KISClient: self.ensure_token() url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor" headers = self._get_headers(tr_id="FHKST01010900") - + params = { "FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": ticker } - + try: res = requests.get(url, headers=headers, params=params) res.raise_for_status() data = res.json() if data['rt_cd'] != '0': return None - - # output ๋ฆฌ์ŠคํŠธ: [ {stck_bsop_date: ๋‚ ์งœ, frgn_ntby_qty: ์™ธ์ธ์ˆœ๋งค์ˆ˜, orgn_ntby_qty: ๊ธฐ๊ด€์ˆœ๋งค์ˆ˜, ...}, ... ] + trends = [] - for item in data['output'][:5]: # ์ตœ๊ทผ 5์ผ์น˜๋งŒ + for item in data['output'][:5]: trends.append({ "date": item['stck_bsop_date'], - "foreigner": self._safe_int(item.get('frgn_ntby_qty')), # ์™ธ์ธ ์ˆœ๋งค์ˆ˜๋Ÿ‰ - "institutional": self._safe_int(item.get('orgn_ntby_qty')), # ๊ธฐ๊ด€ ์ˆœ๋งค์ˆ˜๋Ÿ‰ - "price_change": float(item['prdy_vrss']) # ์ „์ผ๋Œ€๋น„ ๋“ฑ๋ฝ๊ธˆ์•ก + "foreigner": self._safe_int(item.get('frgn_ntby_qty')), + "institutional": self._safe_int(item.get('orgn_ntby_qty')), + "price_change": float(item['prdy_vrss']) }) - - # ์ตœ๊ทผ์ผ์ด 0๋ฒˆ ์ธ๋ฑ์Šค์ž„ + return trends except Exception as e: - print(f"โŒ ํˆฌ์ž์ž ๋™ํ–ฅ ์กฐํšŒ ์‹คํŒจ({ticker}): {e}") + print(f"[KIS] ํˆฌ์ž์ž ๋™ํ–ฅ ์กฐํšŒ ์‹คํŒจ({ticker}): {e}") return None + + +class KISAsyncClient: + """ + ๋น„๋™๊ธฐ KIS API ํด๋ผ์ด์–ธํŠธ + - aiohttp ๊ธฐ๋ฐ˜ HTTP ํ˜ธ์ถœ + - ๋™๊ธฐ KISClient์˜ ํ† ํฐ/์„ค์ •์„ ๊ณต์œ  + - ๋‹ค์ค‘ ์ข…๋ชฉ ๋ณ‘๋ ฌ ์ˆ˜์ง‘์šฉ + """ + def __init__(self, sync_client): + self.sync = sync_client + self.min_interval = 0.5 # ์ดˆ๋‹น 2ํšŒ ์ œํ•œ + + async def _async_get(self, session, url, headers, params): + """๋น„๋™๊ธฐ GET ์š”์ฒญ""" + try: + async with session.get(url, headers=headers, params=params) as resp: + return await resp.json() + except Exception as e: + print(f"[KIS Async] Request failed: {e}") + return None + + async def get_daily_price_async(self, ticker): + """๋น„๋™๊ธฐ ์ผ๋ณ„ ์‹œ์„ธ ์กฐํšŒ""" + import aiohttp + import asyncio + + self.sync.ensure_token() + url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price" + headers = self.sync._get_headers(tr_id="FHKST01010400") + params = { + "FID_COND_MRKT_DIV_CODE": "J", + "FID_INPUT_ISCD": ticker, + "FID_PERIOD_DIV_CODE": "D", + "FID_ORG_ADJ_PRC": "1" + } + + async with aiohttp.ClientSession() as session: + data = await self._async_get(session, url, headers, params) + if data and data.get('rt_cd') == '0': + prices = [int(item['stck_clpr']) for item in data['output']] + prices.reverse() + return prices + return [] + + async def get_investor_trend_async(self, ticker): + """๋น„๋™๊ธฐ ํˆฌ์ž์ž ๋™ํ–ฅ ์กฐํšŒ""" + import aiohttp + + self.sync.ensure_token() + url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor" + headers = self.sync._get_headers(tr_id="FHKST01010900") + params = { + "FID_COND_MRKT_DIV_CODE": "J", + "FID_INPUT_ISCD": ticker + } + + async with aiohttp.ClientSession() as session: + data = await self._async_get(session, url, headers, params) + if data and data.get('rt_cd') == '0': + trends = [] + for item in data['output'][:5]: + trends.append({ + "date": item['stck_bsop_date'], + "foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')), + "institutional": self.sync._safe_int(item.get('orgn_ntby_qty')), + "price_change": float(item['prdy_vrss']) + }) + return trends + return None + + async def get_daily_prices_batch(self, tickers): + """์—ฌ๋Ÿฌ ์ข…๋ชฉ์˜ ์ผ๋ณ„ ์‹œ์„ธ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ""" + import aiohttp + import asyncio + + self.sync.ensure_token() + results = {} + + async with aiohttp.ClientSession() as session: + tasks = [] + for i, ticker in enumerate(tickers): + # rate limit: 0.5์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์š”์ฒญ ์ƒ์„ฑ + if i > 0: + await asyncio.sleep(self.min_interval) + + url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price" + headers = self.sync._get_headers(tr_id="FHKST01010400") + params = { + "FID_COND_MRKT_DIV_CODE": "J", + "FID_INPUT_ISCD": ticker, + "FID_PERIOD_DIV_CODE": "D", + "FID_ORG_ADJ_PRC": "1" + } + tasks.append((ticker, self._async_get(session, url, headers, params))) + + for ticker, task in tasks: + data = await task + if data and data.get('rt_cd') == '0': + prices = [int(item['stck_clpr']) for item in data['output']] + prices.reverse() + results[ticker] = prices + else: + results[ticker] = [] + + return results + + async def get_investor_trends_batch(self, tickers): + """์—ฌ๋Ÿฌ ์ข…๋ชฉ์˜ ํˆฌ์ž์ž ๋™ํ–ฅ์„ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ""" + import aiohttp + import asyncio + + self.sync.ensure_token() + results = {} + + async with aiohttp.ClientSession() as session: + tasks = [] + for i, ticker in enumerate(tickers): + if i > 0: + await asyncio.sleep(self.min_interval) + + url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor" + headers = self.sync._get_headers(tr_id="FHKST01010900") + params = { + "FID_COND_MRKT_DIV_CODE": "J", + "FID_INPUT_ISCD": ticker + } + tasks.append((ticker, self._async_get(session, url, headers, params))) + + for ticker, task in tasks: + data = await task + if data and data.get('rt_cd') == '0': + trends = [] + for item in data['output'][:5]: + trends.append({ + "date": item['stck_bsop_date'], + "foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')), + "institutional": self.sync._safe_int(item.get('orgn_ntby_qty')), + "price_change": float(item['prdy_vrss']) + }) + results[ticker] = trends + else: + results[ticker] = None + + return results diff --git a/modules/services/news.py b/modules/services/news.py index 3fa18b0..2cf6de4 100644 --- a/modules/services/news.py +++ b/modules/services/news.py @@ -1,11 +1,10 @@ +import time import requests import xml.etree.ElementTree as ET + class NewsCollector: - """ - NAS์—์„œ ๋‰ด์Šค๋ฅผ ๋ฐ›์ง€ ๋ชปํ•  ๊ฒฝ์šฐ, Windows ์„œ๋ฒ„์—์„œ ์ง์ ‘ ๋‰ด์Šค๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ๋ชจ๋“ˆ - (Google News RSS ํ™œ์šฉ) - """ + """๋™๊ธฐ ๋‰ด์Šค ์ˆ˜์ง‘ (Google News RSS)""" @staticmethod def get_market_news(query="์ฃผ์‹ ์‹œ์žฅ"): url = f"https://news.google.com/rss/search?q={query}&hl=ko&gl=KR&ceid=KR:ko" @@ -18,5 +17,56 @@ class NewsCollector: items.append({"title": title, "source": "Google News"}) return items except Exception as e: - print(f"โŒ ๋‰ด์Šค ์ˆ˜์ง‘ ์‹คํŒจ: {e}") + print(f"[News] Collection failed: {e}") return [] + + +class AsyncNewsCollector: + """๋น„๋™๊ธฐ ๋‰ด์Šค ์ˆ˜์ง‘ + 5๋ถ„ ์บ์‹ฑ""" + + def __init__(self): + self._cache = None + self._cache_time = 0 + self._cache_ttl = 300 # 5๋ถ„ + + def get_market_news(self, query="์ฃผ์‹ ์‹œ์žฅ"): + """๋™๊ธฐ ์ธํ„ฐํŽ˜์ด์Šค (ํ•˜์œ„ ํ˜ธํ™˜)""" + now = time.time() + if self._cache and (now - self._cache_time) < self._cache_ttl: + return self._cache + + result = NewsCollector.get_market_news(query) + self._cache = result + self._cache_time = now + return result + + async def get_market_news_async(self, query="์ฃผ์‹ ์‹œ์žฅ"): + """๋น„๋™๊ธฐ ๋‰ด์Šค ์ˆ˜์ง‘ (aiohttp + ์บ์‹ฑ)""" + now = time.time() + if self._cache and (now - self._cache_time) < self._cache_ttl: + return self._cache + + try: + import aiohttp + url = f"https://news.google.com/rss/search?q={query}&hl=ko&gl=KR&ceid=KR:ko" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp: + content = await resp.read() + root = ET.fromstring(content) + items = [] + for item in root.findall(".//item")[:5]: + title = item.find("title").text + items.append({"title": title, "source": "Google News"}) + + self._cache = items + self._cache_time = now + return items + except ImportError: + # aiohttp ๋ฏธ์„ค์น˜ ์‹œ ๋™๊ธฐ fallback + return self.get_market_news(query) + except Exception as e: + print(f"[News Async] Collection failed: {e}") + # ์บ์‹œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ๋™๊ธฐ fallback + if self._cache: + return self._cache + return self.get_market_news(query) diff --git a/modules/services/ollama.py b/modules/services/ollama.py index 35d3350..fe4fded 100644 --- a/modules/services/ollama.py +++ b/modules/services/ollama.py @@ -23,7 +23,7 @@ class OllamaManager: try: if pynvml: pynvml.nvmlInit() - self.handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 0๋ฒˆ GPU (3070 Ti) + self.handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 0๋ฒˆ GPU (5070 Ti) self.gpu_available = True print("โœ… [OllamaManager] NVIDIA GPU Monitoring On") else: @@ -74,16 +74,34 @@ class OllamaManager: print(f"โš ๏ธ GPU Status Check Failed: {e}") return {"name": "N/A", "temp": 0, "vram_used": 0, "vram_total": 0, "load": 0} + def is_training_active(self): + """LSTM ํ•™์Šต ์ค‘์ธ์ง€ ํ™•์ธ (GPU ๋ฉ”๋ชจ๋ฆฌ ์ถฉ๋Œ ๋ฐฉ์ง€)""" + try: + import torch + if torch.cuda.is_available(): + # VRAM ์‚ฌ์šฉ๋Ÿ‰์œผ๋กœ ํ•™์Šต ์—ฌ๋ถ€ ์ถ”์ • + vram = self.check_vram() + return vram > Config.VRAM_WARNING_THRESHOLD + except Exception: + pass + return False + def request_inference(self, prompt, context_data=None): """ Ollama์— ์ถ”๋ก  ์š”์ฒญ - :param prompt: ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + ์‚ฌ์šฉ์ž ์ž…๋ ฅ - :param context_data: (Optional) ์ด์ „ ๋Œ€ํ™” ์ปจํ…์ŠคํŠธ + - LSTM ํ•™์Šต ์ค‘์ด๋ฉด ๋Œ€๊ธฐ (GPU ๋ฉ”๋ชจ๋ฆฌ ์ถฉ๋Œ ๋ฐฉ์ง€) """ - # [5070Ti ์ตœ์ ํ™”] VRAM์ด 14GB ์ด์ƒ์ด๋ฉด ๋ชจ๋ธ ์–ธ๋กœ๋“œ ์‹œ๋„ (16GB ์ค‘ ์—ฌ์œ ๋ถ„ ํ™•๋ณด) + # LSTM ํ•™์Šต ์ค‘์ด๋ฉด ์ตœ๋Œ€ 60์ดˆ ๋Œ€๊ธฐ + import time as _time + for _ in range(12): + if not self.is_training_active(): + break + print("[Ollama] Waiting for LSTM training to finish...") + _time.sleep(5) + vram = self.check_vram() - if vram > 14.0: - print(f"โš ๏ธ [OllamaManager] High VRAM Usage ({vram:.1f}GB). Requesting unload.") + if vram > Config.VRAM_WARNING_THRESHOLD: + print(f"[OllamaManager] High VRAM Usage ({vram:.1f}GB). Requesting unload.") try: # keep_alive=0์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ชจ๋ธ ์ฆ‰์‹œ ์–ธ๋กœ๋“œ requests.post(self.generate_url, diff --git a/modules/services/telegram_bot/runner.py b/modules/services/telegram_bot/runner.py index 1314f54..21c1bbd 100644 --- a/modules/services/telegram_bot/runner.py +++ b/modules/services/telegram_bot/runner.py @@ -4,72 +4,73 @@ """ import os import sys +import time import multiprocessing from dotenv import load_dotenv -# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ load_dotenv() -def run_telegram_bot_standalone(): + +def run_telegram_bot_standalone(ipc_lock=None, command_queue=None, shutdown_event=None): """ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡๋งŒ ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰""" - # ๊ฒฝ๋กœ ๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ์ƒ์œ„ ๋””๋ ‰ํ† ๋ฆฌ ์ถ”๊ฐ€ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../'))) from modules.services.telegram_bot.server import TelegramBotServer - from modules.utils.ipc import BotIPC - from modules.config import Config + from modules.utils.ipc import SharedIPC from modules.utils.process_tracker import ProcessTracker token = os.getenv("TELEGRAM_BOT_TOKEN") if not token: - print("โŒ [Telegram] TELEGRAM_BOT_TOKEN not found in .env") + print("[Telegram] TELEGRAM_BOT_TOKEN not found in .env") sys.exit(1) - + ProcessTracker.register("Telegram Bot Standalone") - print(f"๐Ÿค– [Telegram Bot Process] Starting... (PID: {os.getpid()})") - print(f"๐Ÿ”— [Telegram Bot] Standalone Process Mode (IPC Enabled)") - - # IPC ์ดˆ๊ธฐํ™” - ipc = BotIPC() - - # [์ตœ์ ํ™”] ์žฌ์‹œ์ž‘ ๋ฃจํ”„ ๊ตฌํ˜„ + print(f"[Telegram Bot Process] Starting... (PID: {os.getpid()})") + + # IPC ์ดˆ๊ธฐํ™” (shared memory + command queue) + ipc = SharedIPC(lock=ipc_lock, command_queue=command_queue) + while True: + # shutdown ์ฒดํฌ + if shutdown_event and shutdown_event.is_set(): + print("[Telegram Bot] Shutdown signal received.") + break + try: - # ๋ด‡ ์„œ๋ฒ„ ์ƒ์„ฑ (๋งค๋ฒˆ ์ƒˆ๋กœ ์ƒ์„ฑ) - bot_server = TelegramBotServer(token) - - # IPC๋ฅผ ํ†ตํ•ด ๋ฉ”์ธ ๋ด‡ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + bot_server = TelegramBotServer(token, ipc=ipc, shutdown_event=shutdown_event) + + # ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ try: - # ์ดˆ๊ธฐ ์—ฐ๊ฒฐ ์‹œ๋„ instance_data = ipc.get_bot_instance_data() if instance_data: bot_server.set_bot_instance(instance_data) except Exception: - pass # ์—ฐ๊ฒฐ ์‹คํŒจํ•ด๋„ ์ผ๋‹จ ๋ด‡์€ ๋„์›€ - - # ๋ด‡ ์‹คํ–‰ (๋ธ”๋กœํ‚น) + pass + bot_server.run() - - # ์žฌ์‹œ์ž‘ ์š”์ฒญ ํ™•์ธ + if bot_server.should_restart: - print("๐Ÿ”„ [Telegram Bot] Restarting instance...") - import time - time.sleep(1) # ์ž ์‹œ ๋Œ€๊ธฐ + print("[Telegram Bot] Restarting instance...") + time.sleep(1) continue else: - print("๐Ÿ›‘ [Telegram Bot] Process exiting.") + print("[Telegram Bot] Process exiting.") break - + except KeyboardInterrupt: - print("\n๐Ÿ›‘ [Telegram Bot] Stopped by user") + print("[Telegram Bot] Stopped by user") break except Exception as e: if "Conflict" not in str(e): - print(f"โŒ [Telegram Bot] Error: {e}") + print(f"[Telegram Bot] Error: {e}") import traceback traceback.print_exc() break + # ์ •๋ฆฌ + ipc.cleanup() + + if __name__ == "__main__": multiprocessing.freeze_support() run_telegram_bot_standalone() diff --git a/modules/services/telegram_bot/server.py b/modules/services/telegram_bot/server.py index 48b8070..d4c1397 100644 --- a/modules/services/telegram_bot/server.py +++ b/modules/services/telegram_bot/server.py @@ -1,54 +1,49 @@ """ -ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ ์ตœ์ ํ™” ๋ฒ„์ „ -- Polling ์ตœ์ ํ™” (CPU ์‚ฌ์šฉ๋ฅ  ๊ฐ์†Œ) -- ๋ณ„๋„ ํ”„๋กœ์„ธ์Šค๋กœ ๋ถ„๋ฆฌ -- ๋ด‡ ์žฌ์‹œ์ž‘ ๋ช…๋ น์–ด -- ์›๊ฒฉ ๋ช…๋ น์–ด ์‹คํ–‰ +ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ - Shared Memory IPC + ์–‘๋ฐฉํ–ฅ ๋ช…๋ น ์ฑ„๋„ """ import os import asyncio import logging import subprocess -import sys from telegram import Update from telegram.ext import Application, CommandHandler, ContextTypes -from dotenv import load_dotenv - -# ๋กœ๊น… ์„ค์ • logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logging.getLogger("httpx").setLevel(logging.WARNING) + class TelegramBotServer: - def __init__(self, bot_token): - # [์ตœ์ ํ™”] ์—ฐ๊ฒฐ ํ’€ ์„ค์ • ์ถ”๊ฐ€ + def __init__(self, bot_token, ipc=None, shutdown_event=None): self.application = Application.builder()\ .token(bot_token)\ .concurrent_updates(True)\ .build() - + self.bot_instance = None + self.ipc = ipc + self.shutdown_event = shutdown_event self.is_shutting_down = False self.should_restart = False def set_bot_instance(self, bot): - """AutoTradingBot ์ธ์Šคํ„ด์Šค๋ฅผ ์ฃผ์ž…๋ฐ›์Œ""" self.bot_instance = bot def refresh_bot_instance(self): """IPC์—์„œ ์ตœ์‹  ๋ด‡ ์ธ์Šคํ„ด์Šค ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ""" - from modules.utils.ipc import BotIPC - ipc = BotIPC() - self.bot_instance = ipc.get_bot_instance_data() + if self.ipc: + self.bot_instance = self.ipc.get_bot_instance_data() + else: + # fallback: ์ƒˆ IPC ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ + from modules.utils.ipc import SharedIPC + ipc = SharedIPC() + self.bot_instance = ipc.get_bot_instance_data() return self.bot_instance is not None async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/start ๋ช…๋ น์–ด ํ•ธ๋“ค๋Ÿฌ""" - print(f"๐Ÿ“จ [Telegram] /start received from user {update.effective_user.id}") await update.message.reply_text( - "๐Ÿค– AI Trading Bot Command Center\n" + "AI Trading Bot Command Center\n" "๋ช…๋ น์–ด ๋ชฉ๋ก:\n" "/status - ํ˜„์žฌ ๋ด‡ ๋ฐ ์‹œ์žฅ ์ƒํƒœ ์กฐํšŒ\n" "/portfolio - ํ˜„์žฌ ๋ณด์œ  ์ข…๋ชฉ ๋ฐ ํ‰๊ฐ€์•ก\n" @@ -58,151 +53,170 @@ class TelegramBotServer: "/system - PC ๋ฆฌ์†Œ์Šค(CPU/GPU) ์ƒํƒœ\n" "/ai - AI ๋ชจ๋ธ ํ•™์Šต ์ƒํƒœ ์กฐํšŒ\n\n" "[๊ด€๋ฆฌ ๋ช…๋ น์–ด]\n" - "/restart - ๋ด‡ ์žฌ์‹œ์ž‘\n" + "/restart - ๋ฉ”์ธ ๋ด‡ ์žฌ์‹œ์ž‘ ์š”์ฒญ\n" "/exec ๋ช…๋ น์–ด - ์›๊ฒฉ ๋ช…๋ น์–ด ์‹คํ–‰\n" "/stop - ๋ด‡ ์ข…๋ฃŒ", parse_mode="HTML" ) async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/status: ์ข…ํ•ฉ ์ƒํƒœ ๋ธŒ๋ฆฌํ•‘""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") + await update.message.reply_text("๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") return from datetime import datetime now = datetime.now() is_market_open = (9 <= now.hour < 15) or (now.hour == 15 and now.minute < 30) - - status_msg = "โœ… System Status: ONLINE\n" - status_msg += f"๐Ÿ•’ Market: {'OPEN ๐ŸŸข' if is_market_open else 'CLOSED ๐Ÿ”ด'}\n" - + + status_msg = "System Status: ONLINE\n" + status_msg += f"Market: {'OPEN' if is_market_open else 'CLOSED'}\n" + macro_warn = self.bot_instance.is_macro_warning_sent - status_msg += f"๐ŸŒ Macro Filter: {'DANGER ๐Ÿšจ (Trading Halted)' if macro_warn else 'SAFE ๐ŸŸข'}\n" - + status_msg += f"Macro Filter: {'DANGER (Trading Halted)' if macro_warn else 'SAFE'}\n" + await update.message.reply_text(status_msg, parse_mode="HTML") async def portfolio_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/portfolio: ์ž”๊ณ  ์กฐํšŒ""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ด‡ ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") + await update.message.reply_text("๋ด‡ ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") return - - await update.message.reply_text("โณ ์ž”๊ณ ๋ฅผ ์กฐํšŒ ์ค‘์ž…๋‹ˆ๋‹ค...") - + + await update.message.reply_text("์ž”๊ณ ๋ฅผ ์กฐํšŒ ์ค‘์ž…๋‹ˆ๋‹ค...") + try: balance = self.bot_instance.kis.get_balance() if "error" in balance: - await update.message.reply_text(f"โŒ ์ž”๊ณ  ์กฐํšŒ ์‹คํŒจ: {balance['error']}") + await update.message.reply_text(f"์ž”๊ณ  ์กฐํšŒ ์‹คํŒจ: {balance['error']}") return - - msg = f"๐Ÿ’ฐ Total Asset: {int(balance['total_eval']):,} KRW\n" \ - f"๐Ÿ’ต Deposit: {int(balance['deposit']):,} KRW\n\n" - + + msg = f"Total Asset: {int(balance['total_eval']):,} KRW\n" \ + f"Deposit: {int(balance['deposit']):,} KRW\n\n" + if balance['holdings']: msg += "[Holdings]\n" for stock in balance['holdings']: - icon = "๐Ÿ”ด" if stock['yield'] > 0 else "๐Ÿ”ต" - msg += f"{icon} {stock['name']} {stock['yield']}%\n" \ - f" (์ˆ˜๋Ÿ‰: {stock['qty']} / ํ‰๊ฐ€์†์ต: {stock['profit_loss']:,})\n" + yld = float(stock.get('yield', 0)) + # ์ƒ์Šน(๋นจ๊ฐ•), ํ•˜๋ฝ(ํŒŒ๋ž‘) ์ด๋ชจ์ง€ ์ ์šฉ + if yld > 0: + icon = "๐Ÿ”ด" + yld_str = f"+{yld}" + elif yld < 0: + icon = "๐Ÿ”ต" + yld_str = f"{yld}" + else: + icon = "โšช" + yld_str = f"{yld}" + + msg += f"{icon} {stock['name']}: {yld_str}%\n" \ + f" (์ˆ˜๋Ÿ‰: {stock['qty']} / ์†์ต: {stock['profit_loss']:,})\n" else: msg += "๋ณด์œ  ์ค‘์ธ ์ข…๋ชฉ์ด ์—†์Šต๋‹ˆ๋‹ค." - + await update.message.reply_text(msg, parse_mode="HTML") - + except Exception as e: - await update.message.reply_text(f"โŒ Error: {str(e)}") + await update.message.reply_text(f"Error: {str(e)}") async def watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/watchlist: ๊ฐ์‹œ ๋Œ€์ƒ ์ข…๋ชฉ""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ด‡ ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") + await update.message.reply_text("๋ด‡ ์ธ์Šคํ„ด์Šค๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") return target_dict = self.bot_instance.load_watchlist() - discovered = list(self.bot_instance.discovered_stocks) - - msg = f"๐Ÿ‘€ Watchlist: {len(target_dict)} items\n" + discovered = self.bot_instance.discovered_stocks + + msg = f"Watchlist: {len(target_dict)} items\n" for code, name in target_dict.items(): themes = self.bot_instance.theme_manager.get_themes(code) theme_str = f" ({', '.join(themes)})" if themes else "" - msg += f"- {name}{theme_str}\n" - + msg += f"โ€ข {name}{theme_str}\n" + if discovered: - msg += f"\nโœจ Discovered Today ({len(discovered)}):\n" + msg += f"\nDiscovered Today ({len(discovered)}):\n" for code in discovered: msg += f"- {code}\n" - + await update.message.reply_text(msg, parse_mode="HTML") async def update_watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/update_watchlist: Watchlist ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ""" - await update.message.reply_text("๐Ÿ”„ Watchlist๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค... (30์ดˆ ์†Œ์š”)") - - try: - from modules.services.kis import KISClient - from watchlist_manager import WatchlistManager - from modules.config import Config - - temp_kis = KISClient() - mgr = WatchlistManager(temp_kis, watchlist_file=Config.WATCHLIST_FILE) - - summary = mgr.update_watchlist_daily() - # HTML ํŠน์ˆ˜๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ - summary = summary.replace("&", "&").replace("<", "<").replace(">", ">") - await update.message.reply_text(summary) - - except Exception as e: - await update.message.reply_text(f"โŒ ์—…๋ฐ์ดํŠธ ์‹คํŒจ: {e}") + """Watchlist ์—…๋ฐ์ดํŠธ - command queue๋ฅผ ํ†ตํ•ด ๋ฉ”์ธ ๋ด‡์— ์š”์ฒญ""" + if self.ipc and self.ipc.send_command('update_watchlist'): + await update.message.reply_text("Watchlist ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฉ”์ธ ๋ด‡์— ์š”์ฒญํ–ˆ์Šต๋‹ˆ๋‹ค.") + else: + # fallback: ์ง์ ‘ ์—…๋ฐ์ดํŠธ + await update.message.reply_text("Watchlist๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค... (30์ดˆ ์†Œ์š”)") + try: + from modules.services.kis import KISClient + from watchlist_manager import WatchlistManager + from modules.config import Config + + temp_kis = KISClient() + mgr = WatchlistManager(temp_kis, watchlist_file=Config.WATCHLIST_FILE) + summary = mgr.update_watchlist_daily() + summary = summary.replace("&", "&").replace("<", "<").replace(">", ">") + await update.message.reply_text(summary) + except Exception as e: + await update.message.reply_text(f"์—…๋ฐ์ดํŠธ ์‹คํŒจ: {e}") async def macro_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/macro: ๊ฑฐ์‹œ๊ฒฝ์ œ ์ง€ํ‘œ ์กฐํšŒ (IPC ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ)""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ฉ”์ธ ๋ด‡ ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์ค‘...") + await update.message.reply_text("๋ฉ”์ธ ๋ด‡ ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์ค‘...") return - - await update.message.reply_text("โณ ๊ฑฐ์‹œ๊ฒฝ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค...") - + + await update.message.reply_text("๊ฑฐ์‹œ๊ฒฝ์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค...") + try: indices = getattr(self.bot_instance.kis, '_macro_indices', {}) - + if not indices: - await update.message.reply_text("โš ๏ธ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์ˆ˜์ง‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.") - return + await update.message.reply_text("๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์ˆ˜์ง‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") + return status = "SAFE" msi = indices.get('MSI', 0) - if msi >= 50: status = "DANGER" - elif msi >= 30: status = "CAUTION" - - color = "๐ŸŸข" if status == "SAFE" else "๐Ÿ”ด" if status == "DANGER" else "๐ŸŸก" - msg = f"{color} Market Risk: {status}\n\n" - + if msi >= 50: + status = "DANGER" + elif msi >= 30: + status = "CAUTION" + + msg = f"Market Risk: {status}\n\n" + if 'MSI' in indices: - msg += f"๐ŸŒก๏ธ Stress Index: {indices['MSI']}\n" - + msg += f"Stress Index: {indices['MSI']}\n" + for k, v in indices.items(): if k != "MSI": - icon = "๐Ÿ”บ" if v.get('change', 0) > 0 else "๐Ÿ”ป" - msg += f"{icon} {k}: {v.get('price', 0)} ({v.get('change', 0)}%)\n" + change = float(v.get('change', 0)) + price = v.get('price', 0) + if change > 0: + icon = "๐Ÿ”ด" + chg_str = f"+{change}" + elif change < 0: + icon = "๐Ÿ”ต" + chg_str = f"{change}" + else: + icon = "โšช" + chg_str = f"{change}" + + msg += f"{icon} {k}: {price} ({chg_str}%)\n" + await update.message.reply_text(msg, parse_mode="HTML") - + except Exception as e: - await update.message.reply_text(f"โŒ Error: {e}") + await update.message.reply_text(f"Error: {e}") async def system_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/system: ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์ƒํƒœ""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") + await update.message.reply_text("๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") return - + import psutil - - cpu = psutil.cpu_percent(interval=1) + + # non-blocking CPU ์ธก์ • + cpu = psutil.cpu_percent(interval=0) ram = psutil.virtual_memory().percent - - # CPU ์ ์œ ์œจ ์ƒ์œ„ 3๊ฐœ ํ”„๋กœ์„ธ์Šค ์ˆ˜์ง‘ + top_processes = [] for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): try: @@ -212,91 +226,91 @@ class TelegramBotServer: top_processes.append(proc_info) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass - + top_processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True) top_3 = top_processes[:3] - + gpu_status = self.bot_instance.ollama_monitor.get_gpu_status() gpu_msg = "N/A" if gpu_status and gpu_status.get('name') != 'N/A': gpu_name = gpu_status.get('name', 'GPU') - gpu_msg = f"{gpu_name}\n Temp: {gpu_status.get('temp', 0)}ยฐC / VRAM: {gpu_status.get('vram_used', 0)}GB / {gpu_status.get('vram_total', 0)}GB" - - msg = "๐Ÿ–ฅ๏ธ PC System Status\n" \ - f"๐Ÿง  CPU: {cpu}%\n" \ - f"๐Ÿ’พ RAM: {ram}%\n" \ - f"๐ŸŽฎ GPU: {gpu_msg}\n\n" - + gpu_msg = f"{gpu_name}\n Temp: {gpu_status.get('temp', 0)}C / " \ + f"VRAM: {gpu_status.get('vram_used', 0)}GB / {gpu_status.get('vram_total', 0)}GB" + + msg = "PC System Status\n" \ + f"CPU: {cpu}%\n" \ + f"RAM: {ram}%\n" \ + f"GPU: {gpu_msg}\n\n" + if top_3: - msg += "โš™๏ธ Top CPU Processes:\n" + msg += "Top CPU Processes:\n" for i, proc in enumerate(top_3, 1): proc_name = proc.get('name', 'Unknown') proc_cpu = proc.get('cpu_percent', 0) msg += f" {i}. {proc_name} - {proc_cpu:.1f}%\n" - + await update.message.reply_text(msg, parse_mode="HTML") async def ai_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/ai: AI ๋ชจ๋ธ ํ•™์Šต ์ƒํƒœ ์กฐํšŒ""" if not self.refresh_bot_instance(): - await update.message.reply_text("โš ๏ธ ๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") + await update.message.reply_text("๋ฉ”์ธ ๋ด‡์ด ์‹คํ–‰ ์ค‘์ด ์•„๋‹™๋‹ˆ๋‹ค.") return - + gpu = self.bot_instance.ollama_monitor.get_gpu_status() - - msg = "๐Ÿง  AI Model Status\n" - msg += "โ€ข LLM Engine: Ollama (Llama 3.1)\n" - - gpu_name = gpu.get('name', 'NVIDIA RTX 5070 Ti') - msg += f"โ€ข Device: {gpu_name}\n" - + + msg = "AI Model Status\n" + msg += "* LLM Engine: Ollama (Llama 3.1)\n" + msg += f"* Device: {gpu.get('name', 'GPU')}\n" + if gpu: - msg += f"โ€ข GPU Load: {gpu.get('load', 0)}%\n" - msg += f"โ€ข VRAM Usage: {gpu.get('vram_used', 0)}GB / {gpu.get('vram_total', 0)}GB" - + msg += f"* GPU Load: {gpu.get('load', 0)}%\n" + msg += f"* VRAM Usage: {gpu.get('vram_used', 0)}GB / {gpu.get('vram_total', 0)}GB" + await update.message.reply_text(msg, parse_mode="HTML") async def restart_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/restart: ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡ ๋ชจ๋“ˆ๋งŒ ์žฌ์‹œ์ž‘""" - await update.message.reply_text("๐Ÿ”„ ํ…”๋ ˆ๊ทธ๋žจ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...", parse_mode="HTML") - - self.should_restart = True - self.application.stop_running() + """/restart: ๋ฉ”์ธ ๋ด‡์— ์žฌ์‹œ์ž‘ ๋ช…๋ น ์ „๋‹ฌ""" + if self.ipc and self.ipc.send_command('restart'): + await update.message.reply_text( + "๋ฉ”์ธ ๋ด‡์— ์žฌ์‹œ์ž‘ ์š”์ฒญ์„ ์ „์†กํ–ˆ์Šต๋‹ˆ๋‹ค.", parse_mode="HTML") + else: + # IPC ๋ช…๋ น ์‹คํŒจ ์‹œ ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡๋งŒ ์žฌ์‹œ์ž‘ + await update.message.reply_text( + "ํ…”๋ ˆ๊ทธ๋žจ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์žฌ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค...", parse_mode="HTML") + self.should_restart = True + self.application.stop_running() async def stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/stop: ๋ด‡ ์ข…๋ฃŒ""" - await update.message.reply_text("๐Ÿ›‘ ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.", parse_mode="HTML") - + await update.message.reply_text( + "ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์„ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.", parse_mode="HTML") self.should_restart = False self.application.stop_running() async def exec_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): - """/exec: ์›๊ฒฉ ๋ช…๋ น์–ด ์‹คํ–‰ (Non-blocking)""" text = update.message.text.strip() parts = text.split(maxsplit=1) - + if len(parts) < 2: - await update.message.reply_text("โŒ ์‚ฌ์šฉ๋ฒ•: /exec ๋ช…๋ น์–ด") + await update.message.reply_text("์‚ฌ์šฉ๋ฒ•: /exec ๋ช…๋ น์–ด") return - + command = parts[1] - await update.message.reply_text(f"โš™๏ธ ์‹คํ–‰ ์ค‘: {command}", parse_mode="HTML") - + await update.message.reply_text(f"์‹คํ–‰ ์ค‘: {command}", parse_mode="HTML") + try: - # ๋ณด์•ˆ: ์œ„ํ—˜ํ•œ ๋ช…๋ น์–ด ์ฐจ๋‹จ dangerous_keywords = ['rm', 'del', 'format', 'shutdown', 'reboot'] if any(keyword in command.lower() for keyword in dangerous_keywords): - await update.message.reply_text("โ›” ์œ„ํ—˜ํ•œ ๋ช…๋ น์–ด๋Š” ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") + await update.message.reply_text("์œ„ํ—˜ํ•œ ๋ช…๋ น์–ด๋Š” ์‹คํ–‰ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") return - + import platform is_windows = platform.system() == 'Windows' - + if is_windows: exec_cmd = ['powershell', '-Command', command] else: exec_cmd = command - + def run_subprocess(): return subprocess.run( exec_cmd, @@ -308,13 +322,13 @@ class TelegramBotServer: timeout=30, cwd=os.getcwd() ) - + loop = asyncio.get_running_loop() result = await loop.run_in_executor(None, run_subprocess) - + output = result.stdout.strip() if result.stdout else "" error_output = result.stderr.strip() if result.stderr else "" - + if output and error_output: combined = f"[STDOUT]\n{output}\n\n[STDERR]\n{error_output}" elif output: @@ -323,21 +337,19 @@ class TelegramBotServer: combined = f"[ERROR]\n{error_output}" else: combined = "๋ช…๋ น์–ด ์‹คํ–‰ ์™„๋ฃŒ (์ถœ๋ ฅ ์—†์Œ)" - + if len(combined) > 3000: combined = combined[:3000] + "\n... (Truncated)" - - # HTML ํŠน์ˆ˜๋ฌธ์ž ์ด์Šค์ผ€์ดํ”„ + combined = combined.replace("&", "&").replace("<", "<").replace(">", ">") await update.message.reply_text(f"
{combined}
", parse_mode="HTML") - + except asyncio.TimeoutError: - await update.message.reply_text("โฑ๏ธ ๋ช…๋ น์–ด ์‹คํ–‰ ์‹œ๊ฐ„ ์ดˆ๊ณผ (30์ดˆ)") + await update.message.reply_text("๋ช…๋ น์–ด ์‹คํ–‰ ์‹œ๊ฐ„ ์ดˆ๊ณผ (30์ดˆ)") except Exception as e: - await update.message.reply_text(f"โŒ ์‹คํ–‰ ์˜ค๋ฅ˜: {e}") + await update.message.reply_text(f"์‹คํ–‰ ์˜ค๋ฅ˜: {e}") def run(self): - """๋ด‡ ์‹คํ–‰ (Handler ๋“ฑ๋ก ๋ฐ Polling)""" handlers = [ ("start", self.start_command), ("status", self.status_command), @@ -351,26 +363,26 @@ class TelegramBotServer: ("stop", self.stop_command), ("exec", self.exec_command) ] - + for cmd, func in handlers: self.application.add_handler(CommandHandler(cmd, func)) - + async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: if "Conflict" in str(context.error): - print(f"โš ๏ธ [Telegram] Conflict detected. Stopping...") + print(f"[Telegram] Conflict detected. Stopping...") if self.application.running: await self.application.stop() return - print(f"โŒ [Telegram Error] {context.error}") + print(f"[Telegram Error] {context.error}") self.application.add_error_handler(error_handler) - - print("๐Ÿค– [Telegram] Command Server Started (Standard Polling Mode).") - + + print("[Telegram] Command Server Started (Shared Memory IPC Mode).") + try: self.application.run_polling( allowed_updates=Update.ALL_TYPES, drop_pending_updates=True ) except Exception as e: - print(f"โŒ [Telegram] Polling Error: {e}") + print(f"[Telegram] Polling Error: {e}") diff --git a/modules/strategy/process.py b/modules/strategy/process.py index ef39f68..c9d07c0 100644 --- a/modules/strategy/process.py +++ b/modules/strategy/process.py @@ -12,8 +12,10 @@ def get_predictor(): """์›Œ์ปค ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ PricePredictor ์ธ์Šคํ„ด์Šค๋ฅผ ์‹ฑ๊ธ€ํ†ค์œผ๋กœ ๊ด€๋ฆฌ""" global _lstm_predictor if _lstm_predictor is None: - print(f"๐Ÿงฉ [Worker {os.getpid()}] Initializing LSTM Predictor...") + print(f"[Worker {os.getpid()}] Initializing LSTM Predictor...") _lstm_predictor = PricePredictor() + print(f"[Worker {os.getpid()}] LSTM Device: {_lstm_predictor.device}" + f" | AMP: {_lstm_predictor.use_amp}") return _lstm_predictor def analyze_stock_process(ticker, prices, news_items, investor_trend=None): @@ -34,7 +36,7 @@ def analyze_stock_process(ticker, prices, news_items, investor_trend=None): lstm_predictor = get_predictor() if lstm_predictor: lstm_predictor.training_status['current_ticker'] = ticker - pred_result = lstm_predictor.train_and_predict(prices) + pred_result = lstm_predictor.train_and_predict(prices, ticker=ticker) lstm_score = 0.5 # ์ค‘๋ฆฝ ai_confidence = 0.5 diff --git a/modules/utils/ipc.py b/modules/utils/ipc.py index ee1946f..294ca58 100644 --- a/modules/utils/ipc.py +++ b/modules/utils/ipc.py @@ -1,62 +1,126 @@ """ -ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹  (IPC) - ํŒŒ์ผ ๊ธฐ๋ฐ˜ -ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡๊ณผ ๋ฉ”์ธ ๋ด‡ ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ  +ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹  (IPC) - Shared Memory ๊ธฐ๋ฐ˜ +ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡๊ณผ ๋ฉ”์ธ ๋ด‡ ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ  + ์–‘๋ฐฉํ–ฅ ๋ช…๋ น ์ฑ„๋„ """ -import os import json import time -from datetime import datetime +import struct +from multiprocessing.shared_memory import SharedMemory + from modules.config import Config -class BotIPC: - """ํŒŒ์ผ ๊ธฐ๋ฐ˜ IPC (Inter-Process Communication)""" - - def __init__(self, ipc_file=None): - self.ipc_file = ipc_file if ipc_file else Config.IPC_FILE - self.last_update = 0 - + +class SharedIPC: + """Shared Memory + Command Queue ๊ธฐ๋ฐ˜ IPC""" + + def __init__(self, lock=None, command_queue=None): + self.lock = lock + self.command_queue = command_queue + self._shm = None + self._is_creator = False + + def _ensure_shm(self): + """SharedMemory ๋ธ”๋ก์— ์—ฐ๊ฒฐ (์—†์œผ๋ฉด ์ƒ์„ฑ)""" + if self._shm is not None: + return self._shm + try: + self._shm = SharedMemory(name=Config.SHM_NAME, create=False) + except FileNotFoundError: + self._shm = SharedMemory(name=Config.SHM_NAME, create=True, size=Config.SHM_SIZE) + self._is_creator = True + # ์ดˆ๊ธฐํ™”: ๊ธธ์ด ํ•„๋“œ๋ฅผ 0์œผ๋กœ ์„ค์ • + struct.pack_into('I', self._shm.buf, 0, 0) + return self._shm + def write_status(self, data): - """๋ฉ”์ธ ๋ด‡์ด ์ƒํƒœ๋ฅผ ํŒŒ์ผ์— ๊ธฐ๋ก""" + """๋ฉ”์ธ ๋ด‡์ด ์ƒํƒœ๋ฅผ shared memory์— ๊ธฐ๋ก""" try: - with open(self.ipc_file, 'w', encoding='utf-8') as f: - json.dump({ - 'timestamp': time.time(), - 'data': data - }, f, ensure_ascii=False, indent=2) + shm = self._ensure_shm() + payload = json.dumps({ + 'timestamp': time.time(), + 'data': data + }, ensure_ascii=False).encode('utf-8') + + if len(payload) + 4 > Config.SHM_SIZE: + print(f"[IPC] Data too large: {len(payload)} bytes") + return + + if self.lock: + self.lock.acquire() + try: + # [4๋ฐ”์ดํŠธ ๊ธธ์ด][JSON ํŽ˜์ด๋กœ๋“œ] + struct.pack_into('I', shm.buf, 0, len(payload)) + shm.buf[4:4 + len(payload)] = payload + finally: + if self.lock: + self.lock.release() except Exception as e: - print(f"โš ๏ธ [IPC] Write failed: {e}") - + print(f"[IPC] Write failed: {e}") + def read_status(self): - """ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์ด ์ƒํƒœ๋ฅผ ํŒŒ์ผ์—์„œ ์ฝ๊ธฐ""" + """ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์ด ์ƒํƒœ๋ฅผ shared memory์—์„œ ์ฝ๊ธฐ""" try: - if not os.path.exists(self.ipc_file): - print(f"โš ๏ธ [IPC] File not found: {self.ipc_file}") + shm = self._ensure_shm() + + if self.lock: + self.lock.acquire() + try: + length = struct.unpack_from('I', shm.buf, 0)[0] + if length == 0 or length > Config.SHM_SIZE - 4: + return None + raw = bytes(shm.buf[4:4 + length]) + finally: + if self.lock: + self.lock.release() + + ipc_data = json.loads(raw.decode('utf-8')) + age = time.time() - ipc_data.get('timestamp', 0) + + if age > Config.IPC_STALENESS: + print(f"[IPC] Data too old: {age:.1f}s") return None - - with open(self.ipc_file, 'r', encoding='utf-8') as f: - ipc_data = json.load(f) - - # 60์ดˆ ์ด์ƒ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋Š” ๋ฌด์‹œ (10์ดˆ โ†’ 60์ดˆ๋กœ ์™„ํ™”) - timestamp = ipc_data.get('timestamp', 0) - age = time.time() - timestamp - - if age > 60: - print(f"โš ๏ธ [IPC] Data too old: {age:.1f}s") - return None - - print(f"โœ… [IPC] Data loaded (age: {age:.1f}s)") + return ipc_data.get('data') except Exception as e: - print(f"โš ๏ธ [IPC] Read failed: {e}") + print(f"[IPC] Read failed: {e}") return None - + + # --- ๋ช…๋ น ์ฑ„๋„ (ํ…”๋ ˆ๊ทธ๋žจ โ†’ ๋ฉ”์ธ ๋ด‡) --- + + def send_command(self, command, **kwargs): + """ํ…”๋ ˆ๊ทธ๋žจ โ†’ ๋ฉ”์ธ ๋ด‡ ๋ช…๋ น ์ „์†ก""" + if self.command_queue: + try: + self.command_queue.put_nowait({ + 'command': command, + 'timestamp': time.time(), + **kwargs + }) + return True + except Exception as e: + print(f"[IPC] Command send failed: {e}") + return False + + def poll_commands(self): + """๋ฉ”์ธ ๋ด‡์ด ๋ช…๋ น ํ๋ฅผ ํด๋ง""" + commands = [] + if self.command_queue: + try: + while not self.command_queue.empty(): + cmd = self.command_queue.get_nowait() + commands.append(cmd) + except Exception: + pass + return commands + + # --- FakeBot ์ธ์Šคํ„ด์Šค (ํ˜ธํ™˜์„ฑ ์œ ์ง€) --- + def get_bot_instance_data(self): - """๋ด‡ ์ธ์Šคํ„ด์Šค ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (ํ˜ธํ™˜์„ฑ ์œ ์ง€)""" + """๋ด‡ ์ธ์Šคํ„ด์Šค ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (ํ…”๋ ˆ๊ทธ๋žจ ๋ด‡์šฉ)""" status = self.read_status() if not status: return None - - # ๊ฐ€์งœ ๋ด‡ ์ธ์Šคํ„ด์Šค ๊ฐ์ฒด ์ƒ์„ฑ (๊ธฐ์กด ์ฝ”๋“œ ํ˜ธํ™˜) + class FakeBotInstance: def __init__(self, data): self.kis = FakeKIS(data.get('balance', {}), data.get('macro_indices', {})) @@ -66,98 +130,76 @@ class BotIPC: self.is_macro_warning_sent = data.get('is_macro_warning', False) self.watchlist_manager = FakeWatchlistManager(data.get('watchlist', {})) self.load_watchlist = lambda: data.get('watchlist', {}) - + class FakeKIS: def __init__(self, balance_data, macro_indices): self._balance = balance_data if balance_data else { - 'total_eval': 0, - 'deposit': 0, - 'holdings': [] + 'total_eval': 0, 'deposit': 0, 'holdings': [] } self._macro_indices = macro_indices if macro_indices else {} - + def get_balance(self): return self._balance - + def get_current_index(self, ticker): - """์ง€์ˆ˜ ์กฐํšŒ - IPC์—์„œ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜""" if ticker in self._macro_indices: return self._macro_indices[ticker] - # ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ - return { - 'price': 2500.0, - 'change': 0.0 - } - + return {'price': 2500.0, 'change': 0.0} + def get_daily_index_price(self, ticker, period="D"): - """์ง€์ˆ˜ ์ผ๋ณ„ ์‹œ์„ธ ์กฐํšŒ - IPC ๋ชจ๋“œ์—์„œ๋Š” ๋”๋ฏธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜""" - # MacroAnalyzer์˜ MSI ๊ณ„์‚ฐ์šฉ - # ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ๋ฉ”์ธ ๋ด‡์—์„œ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ - # IPC ๋ชจ๋“œ์—์„œ๋Š” ๊ธฐ๋ณธ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (20์ผ์น˜) base_price = 2500.0 if ticker in self._macro_indices: base_price = self._macro_indices[ticker].get('price', 2500.0) - - # 20์ผ์น˜ ๋”๋ฏธ ๋ฐ์ดํ„ฐ (์•ฝ๊ฐ„์˜ ๋ณ€๋™) import random - prices = [] - for i in range(20): - variation = random.uniform(-0.02, 0.02) # ยฑ2% ๋ณ€๋™ - prices.append(base_price * (1 + variation)) - return prices - + return [base_price * (1 + random.uniform(-0.02, 0.02)) for _ in range(20)] + def get_current_price(self, ticker): - """ํ˜„์žฌ๊ฐ€ ์กฐํšŒ - IPC ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€""" return None - + def get_daily_price(self, ticker, period="D"): - """์ผ๋ณ„ ์‹œ์„ธ ์กฐํšŒ - IPC ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€""" return [] - + def get_volume_rank(self, market="0"): - """๊ฑฐ๋ž˜๋Ÿ‰ ์ˆœ์œ„ ์กฐํšŒ - IPC ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€""" return [] - + def buy_stock(self, ticker, qty): - """๋งค์ˆ˜ ์ฃผ๋ฌธ - IPC ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€""" - return {"success": False, "msg": "IPC mode: buy not available"} - + return {"success": False, "msg": "IPC mode"} + def sell_stock(self, ticker, qty): - """๋งค๋„ ์ฃผ๋ฌธ - IPC ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€""" - return {"success": False, "msg": "IPC mode: sell not available"} - + return {"success": False, "msg": "IPC mode"} + class FakeOllama: def __init__(self, gpu_data): self._gpu = gpu_data if gpu_data else { - 'name': 'N/A', - 'temp': 0, - 'vram_used': 0, - 'vram_total': 0, - 'load': 0 + 'name': 'N/A', 'temp': 0, 'vram_used': 0, 'vram_total': 0, 'load': 0 } - + def get_gpu_status(self): return self._gpu - + class FakeThemeManager: def __init__(self, themes_data): self._themes = themes_data if themes_data else {} - + def get_themes(self, ticker): return self._themes.get(ticker, []) - + class FakeWatchlistManager: def __init__(self, watchlist_data): self._watchlist = watchlist_data if watchlist_data else {} - + def update_watchlist_daily(self): - return "โš ๏ธ Watchlist update not available in IPC mode" - + return "Watchlist update not available in IPC mode" + return FakeBotInstance(status) - - def load_watchlist(self): - """Watchlist ๋กœ๋“œ""" - status = self.read_status() - if status: - return status.get('watchlist', {}) - return {} + + def cleanup(self): + """๋ฆฌ์†Œ์Šค ์ •๋ฆฌ""" + if self._shm: + try: + self._shm.close() + if self._is_creator: + self._shm.unlink() + except Exception: + pass + self._shm = None diff --git a/modules/utils/monitor.py b/modules/utils/monitor.py index e6f5155..fb724bc 100644 --- a/modules/utils/monitor.py +++ b/modules/utils/monitor.py @@ -1,7 +1,7 @@ import psutil -import time from datetime import datetime + class SystemMonitor: def __init__(self, messenger, ollama_manager): self.messenger = messenger @@ -9,64 +9,51 @@ class SystemMonitor: self.last_health_check = datetime.now() def check_health(self): - """์‹œ์Šคํ…œ ์ƒํƒœ ์ ๊ฒ€ ๋ฐ ์•Œ๋ฆผ (CPU, RAM, GPU) - 5๋ถ„๋งˆ๋‹ค ์‹คํ–‰""" + """์‹œ์Šคํ…œ ์ƒํƒœ ์ ๊ฒ€ ๋ฐ ์•Œ๋ฆผ (CPU, RAM, GPU) - 3๋ถ„๋งˆ๋‹ค ์‹คํ–‰""" now = datetime.now() - # 5๋ถ„์— ํ•œ ๋ฒˆ์”ฉ๋งŒ ์ฒดํฌ - if (now - self.last_health_check).total_seconds() < 300: + if (now - self.last_health_check).total_seconds() < 180: # 5๋ถ„ โ†’ 3๋ถ„ return self.last_health_check = now alerts = [] - # 1. CPU Check (Double Verify) - # 1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์ธก์ • - cpu_usage = psutil.cpu_percent(interval=1) - + # 1. CPU Check (non-blocking ์ธก์ •) + cpu_usage = psutil.cpu_percent(interval=0) + if cpu_usage > 90: - # ์ผ์‹œ์ ์ธ ์ŠคํŒŒ์ดํฌ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ 3์ดˆ ํ›„ ์žฌ์ธก์ • - time.sleep(3) - cpu_usage_2nd = psutil.cpu_percent(interval=1) - - if cpu_usage_2nd > 90: - # ๊ณผ๋ถ€ํ•˜ ์‹œ ์›์ธ ํ”„๋กœ์„ธ์Šค ์ถ”์  - top_processes = [] - for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): - try: - # Windows ์œ ํœด ํ”„๋กœ์„ธ์Šค ์ œ์™ธ - if proc.info['name'] == 'System Idle Process': - continue - top_processes.append(proc.info) - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - - # CPU ์‚ฌ์šฉ๋ฅ  ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ - top_processes.sort(key=lambda x: x['cpu_percent'], reverse=True) - - # ์ƒ์œ„ ํ”„๋กœ์„ธ์Šค๋“ค์˜ CPU ํ•ฉ๊ณ„ ๊ฒ€์ฆ (์ธก์ • ์˜ค๋ฅ˜ ํ•„ํ„ฐ๋ง) - total_top_cpu = sum(p['cpu_percent'] for p in top_processes[:3]) - if total_top_cpu < 30.0: - print(f"โš ๏ธ [Monitor] Ignored CPU Alert: usage={cpu_usage_2nd}% but top3_sum={total_top_cpu}%") - else: - top_3_str = "" - for p in top_processes[:3]: - top_3_str += f"\n- {p['name']} ({p['cpu_percent']}%)" - - alerts.append(f"๐Ÿ”ฅ **[CPU Overload]** Usage: `{cpu_usage_2nd}%`\n**Top Processes:**{top_3_str}") + # ๊ฒ€์ฆ: ์ƒ์œ„ ํ”„๋กœ์„ธ์Šค CPU ํ•ฉ๊ณ„ ํ™•์ธ + top_processes = [] + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): + try: + if proc.info['name'] == 'System Idle Process': + continue + top_processes.append(proc.info) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + + top_processes.sort(key=lambda x: x['cpu_percent'], reverse=True) + total_top_cpu = sum(p['cpu_percent'] for p in top_processes[:3]) + + if total_top_cpu >= 30.0: + top_3_str = "" + for p in top_processes[:3]: + top_3_str += f"\n- {p['name']} ({p['cpu_percent']}%)" + alerts.append(f"[CPU Overload] Usage: {cpu_usage}%\nTop Processes:{top_3_str}") # 2. RAM Check ram = psutil.virtual_memory() if ram.percent > 90: - alerts.append(f"๐Ÿ’พ **[RAM High]** Usage: `{ram.percent}%` (Free: {ram.available / 1024**3:.1f}GB)") + alerts.append(f"[RAM High] Usage: {ram.percent}% (Free: {ram.available / 1024**3:.1f}GB)") # 3. GPU Check if self.ollama_monitor: gpu_status = self.ollama_monitor.get_gpu_status() temp = gpu_status.get('temp', 0) if temp > 80: - alerts.append(f"โ™จ๏ธ **[GPU Overheat]** Temp: `{temp}ยฐC`") + alerts.append(f"[GPU Overheat] Temp: {temp}C") # ์•Œ๋ฆผ ์ „์†ก if alerts: - msg = "โš ๏ธ **[System Health Alert]**\n" + "\n".join(alerts) + msg = "[System Health Alert]\n" + "\n".join(alerts) if self.messenger: self.messenger.send_message(msg) diff --git a/modules/utils/process_tracker.py b/modules/utils/process_tracker.py index 55f7ce2..4724b2d 100644 --- a/modules/utils/process_tracker.py +++ b/modules/utils/process_tracker.py @@ -1,78 +1,183 @@ +""" +ํ”„๋กœ์„ธ์Šค ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ +- ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ PID ๊ด€๋ฆฌ (pids.txt ํ๊ธฐ) +- Watchdog ํ—ฌ์Šค์ฒดํฌ +- ์ž๋™ ์žฌ์‹œ์ž‘ (์ตœ๋Œ€ 3ํšŒ) +""" import os import time +import threading +from multiprocessing.shared_memory import SharedMemory + +from modules.config import Config + class ProcessTracker: + """๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ํ”„๋กœ์„ธ์Šค ์ถ”์ ๊ธฐ""" + + # ํด๋ž˜์Šค ๋ณ€์ˆ˜: ๋“ฑ๋ก๋œ ํ”„๋กœ์„ธ์Šค ์ •๋ณด + _processes = {} # {name: pid} + _lock = threading.Lock() + + # ํ•˜์œ„ ํ˜ธํ™˜: ๊ธฐ์กด pids.txt ์ •๋ฆฌ์šฉ FILE_PATH = "pids.txt" @staticmethod def register(name): - """ํ˜„์žฌ ํ”„๋กœ์„ธ์Šค์˜ PID์™€ ์ด๋ฆ„์„ ๊ธฐ๋ก""" + """ํ˜„์žฌ ํ”„๋กœ์„ธ์Šค ๋“ฑ๋ก (๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜)""" pid = os.getpid() - entry = f"{pid}: {name} (Started: {time.strftime('%Y-%m-%d %H:%M:%S')})\n" - - try: - # ํŒŒ์ผ์ด ์—†์œผ๋ฉด ์ƒ์„ฑ, ์žˆ์œผ๋ฉด ์ถ”๊ฐ€ - # ๋‹จ, main_server ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ - with open(ProcessTracker.FILE_PATH, "a", encoding="utf-8") as f: - f.write(entry) - print(f"๐Ÿ“Œ Process Registered: {name} (PID: {pid})") - except Exception as e: - print(f"โš ๏ธ Failed to register process: {e}") + with ProcessTracker._lock: + ProcessTracker._processes[name] = pid + print(f"[Process] Registered: {name} (PID: {pid})") + + @staticmethod + def unregister(name): + """ํ”„๋กœ์„ธ์Šค ๋“ฑ๋ก ํ•ด์ œ""" + with ProcessTracker._lock: + ProcessTracker._processes.pop(name, None) + + @staticmethod + def get_all(): + """๋“ฑ๋ก๋œ ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค ๋ฐ˜ํ™˜""" + with ProcessTracker._lock: + return dict(ProcessTracker._processes) @staticmethod def check_and_kill_zombies(): - """ - pids.txt์— ๊ธฐ๋ก๋œ ์ด์ „ ํ”„๋กœ์„ธ์Šค๋“ค์ด ๊ตฌ๋™ ์ค‘์ด๋ผ๋ฉด ๊ฐ•์ œ ์ข…๋ฃŒ. - ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ 1ํšŒ ํ˜ธ์ถœํ•˜์—ฌ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ •๋ฆฌํ•จ. - """ - if not os.path.exists(ProcessTracker.FILE_PATH): - return + """์ด์ „ ์‹คํ–‰์˜ ์ข€๋น„ ํ”„๋กœ์„ธ์Šค ์ •๋ฆฌ + stale SharedMemory ์ •๋ฆฌ""" + # 1. pids.txt ๊ธฐ๋ฐ˜ ์ข€๋น„ ์ •๋ฆฌ (ํ•˜์œ„ ํ˜ธํ™˜) + if os.path.exists(ProcessTracker.FILE_PATH): + try: + import psutil + current_pid = os.getpid() - print("๐Ÿ” Checking for zombie processes...") - try: - import psutil - current_pid = os.getpid() - - with open(ProcessTracker.FILE_PATH, "r", encoding="utf-8") as f: - lines = f.readlines() - - killed_count = 0 - for line in lines: - if ":" not in line or "Running Processes" in line: - continue - - try: - pid_str = line.split(":")[0].strip() - pid = int(pid_str) - - if pid == current_pid: + with open(ProcessTracker.FILE_PATH, "r", encoding="utf-8") as f: + lines = f.readlines() + + killed_count = 0 + for line in lines: + if ":" not in line or "Running Processes" in line: continue - - if psutil.pid_exists(pid): - proc = psutil.Process(pid) - proc_name = proc.name() - - # Python ํ”„๋กœ์„ธ์Šค๋งŒ ํƒ€๊ฒŸ - if "python" in proc_name.lower(): - print(f"๐Ÿ’€ Killing Zombie Process: {pid} ({line.strip()})") - proc.kill() - killed_count += 1 - except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied): - continue - - if killed_count > 0: - print(f"โœ… Cleaned up {killed_count} zombie processes.") - # ํŒŒ์ผ ์ดˆ๊ธฐํ™” - ProcessTracker.clear() - - except Exception as e: - print(f"โš ๏ธ Failed to kill zombies: {e}") + try: + pid = int(line.split(":")[0].strip()) + if pid == current_pid: + continue + if psutil.pid_exists(pid): + proc = psutil.Process(pid) + if "python" in proc.name().lower(): + print(f"[Process] Killing zombie: PID {pid} ({line.strip()})") + proc.kill() + killed_count += 1 + except (ValueError, psutil.NoSuchProcess, psutil.AccessDenied): + continue + + if killed_count > 0: + print(f"[Process] Cleaned up {killed_count} zombie processes.") + except Exception as e: + print(f"[Process] Zombie cleanup failed: {e}") + + # pids.txt ์‚ญ์ œ (๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ) + try: + os.remove(ProcessTracker.FILE_PATH) + except Exception: + pass + + # 2. Stale SharedMemory ์ •๋ฆฌ + try: + shm = SharedMemory(name=Config.SHM_NAME, create=False) + shm.close() + shm.unlink() + print(f"[Process] Cleaned stale SharedMemory: {Config.SHM_NAME}") + except FileNotFoundError: + pass + except Exception: + pass @staticmethod def clear(): - """PID ํŒŒ์ผ ์ดˆ๊ธฐํ™”""" - try: - with open(ProcessTracker.FILE_PATH, "w", encoding="utf-8") as f: - f.write(f"--- Running Processes (Last Update: {time.strftime('%Y-%m-%d %H:%M:%S')}) ---\n") - except: - pass + """๋“ฑ๋ก ์ •๋ณด ์ดˆ๊ธฐํ™”""" + with ProcessTracker._lock: + ProcessTracker._processes.clear() + + +class ProcessWatchdog: + """์ž์‹ ํ”„๋กœ์„ธ์Šค ๊ฐ์‹œ ๋ฐ ์ž๋™ ์žฌ์‹œ์ž‘""" + + def __init__(self, shutdown_event=None): + self.shutdown_event = shutdown_event + self._watched = {} # {name: {process, target, args, restart_count}} + self._thread = None + self._running = False + + def watch(self, name, process, target, args=()): + """ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฐ์‹œ ๋Œ€์ƒ์— ๋“ฑ๋ก""" + self._watched[name] = { + 'process': process, + 'target': target, + 'args': args, + 'restart_count': 0 + } + + def start(self): + """Watchdog ์Šค๋ ˆ๋“œ ์‹œ์ž‘""" + self._running = True + self._thread = threading.Thread(target=self._watchdog_loop, daemon=True) + self._thread.start() + print(f"[Watchdog] Started (interval: {Config.WATCHDOG_INTERVAL}s)") + + def stop(self): + """Watchdog ์ค‘์ง€""" + self._running = False + if self._thread: + self._thread.join(timeout=5) + + def get_process(self, name): + """๊ฐ์‹œ ์ค‘์ธ ํ”„๋กœ์„ธ์Šค ๋ฐ˜ํ™˜""" + entry = self._watched.get(name) + return entry['process'] if entry else None + + def _watchdog_loop(self): + """์ฃผ๊ธฐ์ ์œผ๋กœ ์ž์‹ ํ”„๋กœ์„ธ์Šค ์ƒํƒœ ํ™•์ธ""" + import multiprocessing + + while self._running: + if self.shutdown_event and self.shutdown_event.is_set(): + break + + for name, entry in list(self._watched.items()): + proc = entry['process'] + + if proc.is_alive(): + continue + + # ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ฃฝ์—ˆ์Œ + exit_code = proc.exitcode + restart_count = entry['restart_count'] + + if restart_count >= Config.MAX_RESTART_COUNT: + print(f"[Watchdog] {name} crashed (exit={exit_code}). " + f"Max restarts ({Config.MAX_RESTART_COUNT}) reached. Giving up.") + continue + + print(f"[Watchdog] {name} crashed (exit={exit_code}). " + f"Restarting... ({restart_count + 1}/{Config.MAX_RESTART_COUNT})") + + try: + new_proc = multiprocessing.Process( + target=entry['target'], + args=entry['args'] + ) + new_proc.start() + entry['process'] = new_proc + entry['restart_count'] = restart_count + 1 + print(f"[Watchdog] {name} restarted (new PID: {new_proc.pid})") + except Exception as e: + print(f"[Watchdog] Failed to restart {name}: {e}") + + # ์ธํ„ฐ๋ฒŒ ๋Œ€๊ธฐ (shutdown_event ์ฒดํฌํ•˜๋ฉด์„œ) + for _ in range(Config.WATCHDOG_INTERVAL): + if not self._running: + break + if self.shutdown_event and self.shutdown_event.is_set(): + break + time.sleep(1)