봇 무음 크래시 방지 — trading_loop 예외 처리 + 워치독 추가

- app/main.py: trading_loop while 루프 전체를 try/except로 감싸 예외 발생 시
  로그+Discord 알림 후 루프 재개 (연속 10회 오류 시에만 종료)
- scripts/_watchdog.py: 봇 PID 생존 확인, 죽어있으면 Discord 알림 + 자동 재시작
- scripts/run_watchdog.ps1: 워치독 PowerShell 래퍼
- scripts/setup_scheduler.ps1: StockBot_Watchdog 태스크 등록 추가 (5분 간격)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 10:39:44 +09:00
parent a182c2e70f
commit 6095b4c7fa
4 changed files with 182 additions and 31 deletions
+51 -30
View File
@@ -321,44 +321,65 @@ class StockBot:
"""1초 단위 메인 루프"""
logger.info("매매 루프 시작")
self.running = True
_consecutive_errors = 0
while self.running:
now = datetime.now()
now_str = now.strftime("%H:%M")
try:
now = datetime.now()
now_str = now.strftime("%H:%M")
# 14:50 강제 청산
if now_str >= FORCE_EXIT:
await self.force_exit_all()
self.running = False
break
# 14:50 강제 청산
if now_str >= FORCE_EXIT:
await self.force_exit_all()
self.running = False
break
# 14:00 이후 신규 진입 중단 (청산은 계속)
if now_str > "14:00":
# 14:00 이후 신규 진입 중단 (청산은 계속)
if now_str > "14:00":
await self.check_exits()
await asyncio.sleep(1)
continue
# 09:00 이전 대기
if now_str < "09:00":
await asyncio.sleep(1)
continue
# midday_context.json 갱신 감지 (점심 세션 이벤트 기반 시작)
self._check_midday_context()
# 리스크 체크 (L2/L4/L5 하드 중단)
if not self.risk.can_trade():
await asyncio.sleep(5)
continue
# 보유 포지션 청산 체크
await self.check_exits()
# 신규 진입 체크
if self.risk.can_add_position(len(self.positions)):
await self.check_entries()
_consecutive_errors = 0
await asyncio.sleep(1)
continue
# 09:00 이전 대기
if now_str < "09:00":
await asyncio.sleep(1)
continue
# midday_context.json 갱신 감지 (점심 세션 이벤트 기반 시작)
self._check_midday_context()
# 리스크 체크 (L2/L4/L5 하드 중단)
if not self.risk.can_trade():
except asyncio.CancelledError:
raise
except Exception as e:
_consecutive_errors += 1
logger.error(
f"매매 루프 오류 (연속 {_consecutive_errors}회): "
f"{type(e).__name__}: {e}",
exc_info=True,
)
await notify_error(f"매매 루프 오류 {_consecutive_errors}회: {type(e).__name__}: {e}")
# 연속 10회 오류 시 루프 종료 (무한 오류 방지)
if _consecutive_errors >= 10:
logger.critical("연속 오류 10회 — 매매 루프 강제 종료")
await notify_error("연속 오류 10회 — 매매 루프 강제 종료")
self.running = False
break
await asyncio.sleep(5)
continue
# 보유 포지션 청산 체크
await self.check_exits()
# 신규 진입 체크
if self.risk.can_add_position(len(self.positions)):
await self.check_entries()
await asyncio.sleep(1)
# ─────────────────────────────────────────
# 진입 체크
+81
View File
@@ -0,0 +1,81 @@
"""
watchdog.py — 봇 생존 감시 + 자동 재시작
5분마다 실행 (작업 스케줄러) / 장중(09:00~15:10)에만 작동
"""
import asyncio, os, subprocess, sys
from datetime import datetime
PROJECT = r'C:\Users\whdwo\OneDrive\바탕 화면\stockbot_v3'
PID_FILE = os.path.join(PROJECT, 'logs', 'bot.pid')
os.chdir(PROJECT)
sys.path.insert(0, '.')
from app.main import load_env
load_env()
def is_process_alive(pid: int) -> bool:
r = subprocess.run(
['tasklist', '/FI', f'PID eq {pid}', '/NH'],
capture_output=True, text=True,
)
return str(pid) in r.stdout
def get_pid() -> int | None:
try:
return int(open(PID_FILE).read().strip())
except Exception:
return None
def restart_bot() -> int:
proc = subprocess.Popen(
[sys.executable, 'app/main.py'],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
stdout=open('logs/bot_stderr.log', 'a', encoding='utf-8'),
stderr=subprocess.STDOUT,
close_fds=True,
)
with open(PID_FILE, 'w') as f:
f.write(str(proc.pid))
return proc.pid
async def main():
now = datetime.now()
now_str = now.strftime("%H:%M")
# 장 외 시간은 체크 안 함
if not ("09:00" <= now_str <= "15:10"):
print(f"[{now_str}] 장 외 시간 — 워치독 종료")
return
from app.monitor.notifier import send
pid = get_pid()
if pid is None:
msg = f"[경고] 봇 PID 파일 없음 — 봇이 실행되지 않은 상태입니다 ({now_str})"
print(msg)
await send(msg)
new_pid = restart_bot()
await send(f"[복구] 봇 자동 재시작 완료 PID={new_pid}")
return
if is_process_alive(pid):
print(f"[{now_str}] 봇 정상 실행 중 PID={pid}")
return
# 봇이 죽어있음
msg = f"[긴급] 봇 프로세스 종료 감지 (PID={pid}) — 자동 재시작 시도"
print(msg)
await send(msg)
new_pid = restart_bot()
await send(f"[복구] 봇 자동 재시작 완료 PID={new_pid} ({now_str})")
print(f"봇 재시작 완료 PID={new_pid}")
if __name__ == "__main__":
asyncio.run(main())
+21
View File
@@ -0,0 +1,21 @@
# 워치독 스크립트 — 봇 생존 감시 + 자동 재시작
# 작업 스케줄러에서 5분마다 실행 (평일 09:00~15:10)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8"
$PROJECT = Split-Path -Parent $PSScriptRoot
$LOG = "$PROJECT\logs\watchdog.log"
$utf8 = New-Object System.Text.UTF8Encoding $false
Set-Location $PROJECT
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
[System.IO.File]::AppendAllText($LOG, "[$timestamp] 워치독 실행`n", $utf8)
python scripts/_watchdog.py 2>&1 |
ForEach-Object { [System.IO.File]::AppendAllText($LOG, "$_`n", $utf8) }
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
[System.IO.File]::AppendAllText($LOG, "[$timestamp] 워치독 완료`n", $utf8)
+29 -1
View File
@@ -42,7 +42,7 @@ function Register-StockTask($name, $time, $script, $limitMin) {
Write-Host "[OK] $name 등록 완료 (평일 $time)" -ForegroundColor Green
}
# ── 3개 태스크 등록 (StockBot_Bot 제거 — /morning이 /start-bot을 호출함) ────
# ── 태스크 등록 ────────────────────────────────────────────────────────────────
# 08:15 claude /morning → 뉴스+KIS 수집 → daily_context.json → /start-bot 호출
Register-StockTask "StockBot_Morning" "08:15" "run_morning.ps1" 20
@@ -52,6 +52,34 @@ Register-StockTask "StockBot_Midday" "11:20" "run_midday.ps1" 20
# 15:30 claude /evening → 결과 분석 → 리포트 → Discord
Register-StockTask "StockBot_Evening" "15:30" "run_evening.ps1" 30
# 워치독: 5분마다 봇 생존 확인 → 죽어있으면 Discord 알림 + 자동 재시작
function Register-WatchdogTask {
$times = @("09:00","09:05","09:10","09:15","09:20","09:25","09:30","09:35","09:40","09:45","09:50","09:55",
"10:00","10:05","10:10","10:15","10:20","10:25","10:30","10:35","10:40","10:45","10:50","10:55",
"11:00","11:05","11:10","11:15","11:20","11:25","11:30","11:35","11:40","11:45","11:50","11:55",
"12:00","12:05","12:10","12:15","12:20","12:25","12:30","12:35","12:40","12:45","12:50","12:55",
"13:00","13:05","13:10","13:15","13:20","13:25","13:30","13:35","13:40","13:45","13:50","13:55",
"14:00","14:05","14:10","14:15","14:20","14:25","14:30","14:35","14:40","14:45","14:50","14:55",
"15:00","15:05","15:10")
$triggers = $times | ForEach-Object {
New-ScheduledTaskTrigger -Weekly -DaysOfWeek $weekdays -At $_
}
$action = New-ScheduledTaskAction `
-Execute "powershell.exe" `
-Argument "-NonInteractive -ExecutionPolicy Bypass -File `"$PROJECT\scripts\run_watchdog.ps1`"" `
-WorkingDirectory $PROJECT
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Minutes 3) `
-StartWhenAvailable `
-MultipleInstances IgnoreNew `
-DontStopIfGoingOnBatteries
$settings.DisallowStartIfOnBatteries = $false
Register-ScheduledTask -TaskName "StockBot_Watchdog" -TaskPath "\StockBot\" `
-Trigger $triggers -Action $action -Settings $settings -RunLevel Limited -Force | Out-Null
Write-Host "[OK] StockBot_Watchdog 등록 완료 (평일 09:00~15:10, 5분 간격)" -ForegroundColor Green
}
Register-WatchdogTask
# StockBot_Bot 비활성화 유지 (이미 존재할 경우)
$botTask = Get-ScheduledTask -TaskName "StockBot_Bot" -TaskPath "\StockBot\" -ErrorAction SilentlyContinue
if ($botTask) {