Fix run_session_now: log all exceptions, never silently drop errors

- First log call moved to very top of run_session_now() so user always
  sees the task started, even if subsequent DB/engine calls fail
- Entire function body wrapped in try/except: errors go to scheduler log
  instead of vanishing in asyncio's unhandled-exception machinery
- Endpoint wraps create_task() in a logging shim (_task()) for the same reason
- 'already ran' path now logs a visible warning instead of silent return

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 407238e5d5
commit 84e80fca75

@ -489,8 +489,14 @@ class ORBAutoScheduler:
After detection, injects the session's remaining today-events into the After detection, injects the session's remaining today-events into the
active schedule so breakout checks, stop checks, and EOD exit still fire. active schedule so breakout checks, stop checks, and EOD exit still fire.
Returns a summary dict or {"error": "<reason>"} on failure. All exceptions are caught and logged so the fire-and-forget task never
silently vanishes.
""" """
# Log immediately so the user knows the task started — even before any
# DB / engine work that might fail.
self._log(f"🔄 지금 시작 요청: {session_name}")
try:
from apps.orb_trader.state import ORBStateManager from apps.orb_trader.state import ORBStateManager
now_et = self._now_et() now_et = self._now_et()
@ -500,26 +506,28 @@ class ORBAutoScheduler:
state_mgr = ORBStateManager(self._db_path) state_mgr = ORBStateManager(self._db_path)
session = state_mgr.get_session(session_name) session = state_mgr.get_session(session_name)
if session is None: if session is None:
self._log(f" ❌ 세션 '{session_name}' 을 DB에서 찾을 수 없음")
return {"error": f"Session '{session_name}' not found"} return {"error": f"Session '{session_name}' not found"}
# Already ran if daily_state.phase is set (engine wrote it during orb_detect) # Already ran if daily_state.phase is beyond idle
daily = state_mgr.get_daily_state(session.session_id, date_str) daily = state_mgr.get_daily_state(session.session_id, date_str)
if daily.phase: if daily.phase not in ("idle", "", None):
return {"error": f"Session '{session_name}' already ran today (phase={daily.phase})"} self._log(f"{session_name}: 오늘 이미 실행됨 (phase={daily.phase})")
return {"error": f"already ran today (phase={daily.phase})"}
self._log(f"🔄 지금 시작: {session_name} ORB 감지 실행 중 (현재 시세 기준)...") self._log(f" ORB 감지 실행 중 (현재 시세 기준)...")
# Phase 1: Run ORB detection (fetches historical bars for ORB window) # Phase 1: ORB detection — fetches historical bars for the ORB window
await self._run_trading("orb_detect", [session_name], date_str) await self._run_trading("orb_detect", [session_name], date_str)
# Phase 2: Immediately check for breakouts using current snapshot prices. # Phase 2: Immediate breakout check using current snapshot prices.
# This is the "지금 시작" core regardless of whether scheduled breakout # This is the core of "지금 시작" — the scheduled breakout windows have
# windows have already passed, we check current price right now. # already passed, so we check once manually right now.
self._log(f"🔍 {session_name}: 현재 가격으로 브레이크아웃 즉시 체크...") self._log(f" 현재 가격으로 브레이크아웃 즉시 체크...")
await self._run_trading("breakout", [session_name], date_str) await self._run_trading("breakout", [session_name], date_str)
# Phase 3: Inject remaining future events (stop checks + EOD exit only). # Phase 3: Inject remaining future events (stop checks + EOD exit only).
# Skip all breakout events — we just ran the manual check above. # Mark all scheduled breakout events as done — we already ran one above.
params = _load_session_params(self._db_path, session_name) params = _load_session_params(self._db_path, session_name)
all_events = build_schedule(today, **params) all_events = build_schedule(today, **params)
for ev in all_events: for ev in all_events:
@ -530,7 +538,6 @@ class ORBAutoScheduler:
injected = 0 injected = 0
for ev in all_events: for ev in all_events:
if ev["kind"] == "breakout": if ev["kind"] == "breakout":
# Already handled by the manual check above
self._completed.add(ev["name"]) self._completed.add(ev["name"])
elif ev["et_dt"] <= now_et: elif ev["et_dt"] <= now_et:
self._completed.add(ev["name"]) self._completed.add(ev["name"])
@ -539,10 +546,15 @@ class ORBAutoScheduler:
injected += 1 injected += 1
self._today_schedule.sort(key=lambda e: (e["et_dt"], e.get("session", ""))) self._today_schedule.sort(key=lambda e: (e["et_dt"], e.get("session", "")))
self._log(f"{session_name}: {injected} 이벤트 추가됨 (스톱/EOD 일정)") self._log(f" ✓ 완료 — 스톱/EOD 이벤트 {injected}개 추가됨")
return {"session": session_name, "injected": injected} return {"session": session_name, "injected": injected}
except Exception as exc:
tb = traceback.format_exc()
self._log(f" ❌ 지금 시작 오류: {exc}")
log.error("run_session_now error: %s\n%s", exc, tb)
return {"error": str(exc)}
# ── Main scheduler loop ──────────────────────────────────────────────────── # ── Main scheduler loop ────────────────────────────────────────────────────
async def _run_loop(self) -> None: async def _run_loop(self) -> None:

@ -169,8 +169,17 @@ async def run_session_today(session_id: str) -> dict[str, Any]:
detail="스케줄러가 실행 중이 아닙니다. 먼저 자동 스케줄러를 시작하세요.", detail="스케줄러가 실행 중이 아닙니다. 먼저 자동 스케줄러를 시작하세요.",
) )
# Fire-and-forget — detection takes ~12 min; we return immediately # Fire-and-forget — detection takes ~12 min; we return immediately.
_asyncio.create_task(orb_auto_scheduler.run_session_now(session.session_name)) # Wrap in a logging shim so unhandled exceptions surface in the scheduler log,
# not silently in Python's asyncio warning machinery.
async def _task() -> None:
try:
await orb_auto_scheduler.run_session_now(session.session_name)
except Exception as exc:
import traceback as _tb
orb_auto_scheduler._log(f"❌ run_today 작업 예외: {exc}\n{_tb.format_exc()}")
_asyncio.create_task(_task())
return { return {
"session_id": session.session_id, "session_id": session.session_id,

Loading…
Cancel
Save