봇 무음 크래시 방지 — 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초 단위 메인 루프"""
|
"""1초 단위 메인 루프"""
|
||||||
logger.info("매매 루프 시작")
|
logger.info("매매 루프 시작")
|
||||||
self.running = True
|
self.running = True
|
||||||
|
_consecutive_errors = 0
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
now_str = now.strftime("%H:%M")
|
now_str = now.strftime("%H:%M")
|
||||||
|
|
||||||
@@ -358,8 +360,27 @@ class StockBot:
|
|||||||
if self.risk.can_add_position(len(self.positions)):
|
if self.risk.can_add_position(len(self.positions)):
|
||||||
await self.check_entries()
|
await self.check_entries()
|
||||||
|
|
||||||
|
_consecutive_errors = 0
|
||||||
await asyncio.sleep(1)
|
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
|
Write-Host "[OK] $name 등록 완료 (평일 $time)" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 3개 태스크 등록 (StockBot_Bot 제거 — /morning이 /start-bot을 호출함) ────
|
# ── 태스크 등록 ────────────────────────────────────────────────────────────────
|
||||||
# 08:15 claude /morning → 뉴스+KIS 수집 → daily_context.json → /start-bot 호출
|
# 08:15 claude /morning → 뉴스+KIS 수집 → daily_context.json → /start-bot 호출
|
||||||
Register-StockTask "StockBot_Morning" "08:15" "run_morning.ps1" 20
|
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
|
# 15:30 claude /evening → 결과 분석 → 리포트 → Discord
|
||||||
Register-StockTask "StockBot_Evening" "15:30" "run_evening.ps1" 30
|
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 비활성화 유지 (이미 존재할 경우)
|
# StockBot_Bot 비활성화 유지 (이미 존재할 경우)
|
||||||
$botTask = Get-ScheduledTask -TaskName "StockBot_Bot" -TaskPath "\StockBot\" -ErrorAction SilentlyContinue
|
$botTask = Get-ScheduledTask -TaskName "StockBot_Bot" -TaskPath "\StockBot\" -ErrorAction SilentlyContinue
|
||||||
if ($botTask) {
|
if ($botTask) {
|
||||||
|
|||||||
Reference in New Issue
Block a user