LSTM v3 멀티피처, KIS OHLCV 배치, 동적 전략 강화

- deep_learning.py: INPUT_SIZE=7 (close/open/high/low/volume/rsi/macd),
  feature_scaler/target_scaler 분리, ModelRegistry LRU 종목별 격리 (v3 체크포인트)
- kis.py: get_daily_ohlcv() OHLCV 전체 반환, KISAsyncClient 비동기 배치 조회 추가,
  order() 지정가/조건부 주문 지원
- strategy/process.py: ATR/ADX 기반 동적 손절익절, 트레일링 스탑, 포지션 사이징 강화
- config.py: OLLAMA_NUM_THREAD=8 (9800X3D 최적화), LSTM_COOLDOWN/FAST_EPOCHS 환경변수화
- macro.py: 거시경제 지표 계산 개선
- ollama.py: VRAM 여유량 기반 선택적 언로드
- monitor.py: CPU 서킷 브레이커 연속 횟수 조건 추가
- ipc.py: IPC_STALENESS 600초로 확대
- news.py: 비동기 뉴스 수집 개선
- telegram.py, runner.py: 안정성 개선

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 23:08:33 +09:00
parent 37f6d87bec
commit 4e77a1acf1
11 changed files with 939 additions and 268 deletions

View File

@@ -59,6 +59,7 @@ class SharedIPC:
def read_status(self):
"""텔레그램 봇이 상태를 shared memory에서 읽기"""
raw = None
try:
shm = self._ensure_shm()
@@ -66,13 +67,15 @@ class SharedIPC:
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])
if length > 0 and length <= Config.SHM_SIZE - 4:
raw = bytes(shm.buf[4:4 + length])
finally:
if self.lock:
self.lock.release()
if not raw:
return None
ipc_data = json.loads(raw.decode('utf-8'))
age = time.time() - ipc_data.get('timestamp', 0)

View File

@@ -1,6 +1,8 @@
import psutil
from datetime import datetime
from modules.config import Config
class SystemMonitor:
def __init__(self, messenger, ollama_manager):
@@ -8,52 +10,101 @@ class SystemMonitor:
self.ollama_monitor = ollama_manager
self.last_health_check = datetime.now()
# CPU 서킷 브레이커 상태
self._cpu_overload_count = 0 # 연속 과부하 횟수
self._circuit_open = False # 서킷 브레이커 발동 여부
self._circuit_open_since = None
def is_cpu_critical(self):
"""서킷 브레이커가 발동 상태인지 반환 (True이면 분석 사이클 스킵)"""
return self._circuit_open
def reset_circuit(self):
"""서킷 브레이커 수동 리셋"""
if self._circuit_open:
print("[Monitor] CPU Circuit Breaker RESET")
self._circuit_open = False
self._cpu_overload_count = 0
self._circuit_open_since = None
def check_health(self):
"""시스템 상태 점검 및 알림 (CPU, RAM, GPU) - 3분마다 실행"""
now = datetime.now()
if (now - self.last_health_check).total_seconds() < 180: # 5분 → 3분
if (now - self.last_health_check).total_seconds() < 180:
return
self.last_health_check = now
alerts = []
# 1. CPU Check (non-blocking 측정)
cpu_usage = psutil.cpu_percent(interval=0)
# 1. CPU Check
cpu_usage = psutil.cpu_percent(interval=1) # 1초 측정 (더 정확)
if cpu_usage > 90:
# 검증: 상위 프로세스 CPU 합계 확인
if cpu_usage > Config.CPU_CIRCUIT_BREAKER_THRESHOLD:
self._cpu_overload_count += 1
# 상위 프로세스 조회
top_processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
try:
if proc.info['name'] == 'System Idle Process':
if proc.info['name'] in ('System Idle Process', 'Idle'):
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])
top_3_str = ""
for p in top_processes[:3]:
top_3_str += f"\n- {p['name']} ({p['cpu_percent']}%)"
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}")
# 서킷 브레이커 발동 조건
if self._cpu_overload_count >= Config.CPU_CIRCUIT_BREAKER_CONSECUTIVE:
if not self._circuit_open:
self._circuit_open = True
self._circuit_open_since = now
alerts.append(
f"🔴 [CPU Circuit Breaker OPEN] {cpu_usage}% × {self._cpu_overload_count}회 연속\n"
f"⛔ 분석 사이클 일시 중단 (5분 후 자동 복구)\nTop Processes:{top_3_str}"
)
print(f"[Monitor] CPU Circuit Breaker OPEN! CPU={cpu_usage}%")
else:
alerts.append(
f"⚠️ [CPU Overload] Usage: {cpu_usage}% ({self._cpu_overload_count}회)\nTop Processes:{top_3_str}"
)
else:
# CPU 정상 → 카운터 리셋
if self._cpu_overload_count > 0:
print(f"[Monitor] CPU 정상화 ({cpu_usage}%). 카운터 리셋.")
self._cpu_overload_count = 0
# 서킷 브레이커가 열린 후 5분 경과 시 자동 복구
if self._circuit_open and self._circuit_open_since:
elapsed = (now - self._circuit_open_since).total_seconds()
if elapsed >= 300: # 5분
self._circuit_open = False
self._circuit_open_since = None
alerts.append("✅ [CPU Circuit Breaker CLOSED] 시스템 안정화. 분석 재개.")
print("[Monitor] CPU Circuit Breaker CLOSED. 분석 재개.")
# 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)
if self.messenger:
self.messenger.send_message(msg)
# 콘솔에만 출력
for alert in alerts:
print(f"[Monitor] {alert}")
# [비활성화] 텔레그램 알림 - 필요시 재활성화
# msg = "🔔 <b>[System Health Alert]</b>\n" + "\n".join(alerts)
# if self.messenger:
# self.messenger.send_message(msg)