봇 무음 크래시 방지 — 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:
+21
@@ -321,8 +321,10 @@ class StockBot:
|
||||
"""1초 단위 메인 루프"""
|
||||
logger.info("매매 루프 시작")
|
||||
self.running = True
|
||||
_consecutive_errors = 0
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
now = datetime.now()
|
||||
now_str = now.strftime("%H:%M")
|
||||
|
||||
@@ -358,8 +360,27 @@ class StockBot:
|
||||
if self.risk.can_add_position(len(self.positions)):
|
||||
await self.check_entries()
|
||||
|
||||
_consecutive_errors = 0
|
||||
await asyncio.sleep(1)
|
||||
|
||||
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)
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# 진입 체크
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user