From 84e80fca75e7979ad8e82c011c1c02265499ae05 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Mon, 13 Apr 2026 10:31:35 -0700 Subject: [PATCH] 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 --- apps/web/orb_trading_service.py | 112 ++++++++++++++++++-------------- apps/web/routers/orb_trading.py | 13 +++- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/apps/web/orb_trading_service.py b/apps/web/orb_trading_service.py index 6b3a0d0..b424dcc 100644 --- a/apps/web/orb_trading_service.py +++ b/apps/web/orb_trading_service.py @@ -489,59 +489,71 @@ class ORBAutoScheduler: After detection, injects the session's remaining today-events into the active schedule so breakout checks, stop checks, and EOD exit still fire. - Returns a summary dict or {"error": ""} on failure. + All exceptions are caught and logged so the fire-and-forget task never + silently vanishes. """ - from apps.orb_trader.state import ORBStateManager + # Log immediately so the user knows the task started β€” even before any + # DB / engine work that might fail. + self._log(f"πŸ”„ μ§€κΈˆ μ‹œμž‘ μš”μ²­: {session_name}") - now_et = self._now_et() - today = now_et.date() - date_str = today.isoformat() + try: + from apps.orb_trader.state import ORBStateManager - state_mgr = ORBStateManager(self._db_path) - session = state_mgr.get_session(session_name) - if session is None: - return {"error": f"Session '{session_name}' not found"} - - # Already ran if daily_state.phase is set (engine wrote it during orb_detect) - daily = state_mgr.get_daily_state(session.session_id, date_str) - if daily.phase: - return {"error": f"Session '{session_name}' already ran today (phase={daily.phase})"} - - self._log(f"πŸ”„ μ§€κΈˆ μ‹œμž‘: {session_name} β€” ORB 감지 μ‹€ν–‰ 쀑 (ν˜„μž¬ μ‹œμ„Έ κΈ°μ€€)...") - - # Phase 1: Run ORB detection (fetches historical bars for ORB window) - await self._run_trading("orb_detect", [session_name], date_str) - - # Phase 2: Immediately check for breakouts using current snapshot prices. - # This is the "μ§€κΈˆ μ‹œμž‘" core β€” regardless of whether scheduled breakout - # windows have already passed, we check current price right now. - self._log(f"πŸ” {session_name}: ν˜„μž¬ κ°€κ²©μœΌλ‘œ λΈŒλ ˆμ΄ν¬μ•„μ›ƒ μ¦‰μ‹œ 체크...") - await self._run_trading("breakout", [session_name], date_str) - - # Phase 3: Inject remaining future events (stop checks + EOD exit only). - # Skip all breakout events β€” we just ran the manual check above. - params = _load_session_params(self._db_path, session_name) - all_events = build_schedule(today, **params) - for ev in all_events: - ev["session"] = session_name - ev["name"] = f"{session_name}:{ev['name']}" - - existing_names = {e["name"] for e in self._today_schedule} - injected = 0 - for ev in all_events: - if ev["kind"] == "breakout": - # Already handled by the manual check above - self._completed.add(ev["name"]) - elif ev["et_dt"] <= now_et: - self._completed.add(ev["name"]) - elif ev["name"] not in existing_names: - self._today_schedule.append(ev) - injected += 1 - - self._today_schedule.sort(key=lambda e: (e["et_dt"], e.get("session", ""))) - self._log(f"βœ“ {session_name}: {injected} 이벀트 좔가됨 (μŠ€ν†±/EOD 일정)") - - return {"session": session_name, "injected": injected} + now_et = self._now_et() + today = now_et.date() + date_str = today.isoformat() + + state_mgr = ORBStateManager(self._db_path) + session = state_mgr.get_session(session_name) + if session is None: + self._log(f" ❌ μ„Έμ…˜ '{session_name}' 을 DBμ—μ„œ 찾을 수 μ—†μŒ") + return {"error": f"Session '{session_name}' not found"} + + # Already ran if daily_state.phase is beyond idle + daily = state_mgr.get_daily_state(session.session_id, date_str) + if daily.phase not in ("idle", "", None): + self._log(f" ⚠ {session_name}: 였늘 이미 싀행됨 (phase={daily.phase})") + return {"error": f"already ran today (phase={daily.phase})"} + + self._log(f" ORB 감지 μ‹€ν–‰ 쀑 (ν˜„μž¬ μ‹œμ„Έ κΈ°μ€€)...") + + # Phase 1: ORB detection β€” fetches historical bars for the ORB window + await self._run_trading("orb_detect", [session_name], date_str) + + # Phase 2: Immediate breakout check using current snapshot prices. + # This is the core of "μ§€κΈˆ μ‹œμž‘" β€” the scheduled breakout windows have + # already passed, so we check once manually right now. + self._log(f" ν˜„μž¬ κ°€κ²©μœΌλ‘œ λΈŒλ ˆμ΄ν¬μ•„μ›ƒ μ¦‰μ‹œ 체크...") + await self._run_trading("breakout", [session_name], date_str) + + # Phase 3: Inject remaining future events (stop checks + EOD exit only). + # Mark all scheduled breakout events as done β€” we already ran one above. + params = _load_session_params(self._db_path, session_name) + all_events = build_schedule(today, **params) + for ev in all_events: + ev["session"] = session_name + ev["name"] = f"{session_name}:{ev['name']}" + + existing_names = {e["name"] for e in self._today_schedule} + injected = 0 + for ev in all_events: + if ev["kind"] == "breakout": + self._completed.add(ev["name"]) + elif ev["et_dt"] <= now_et: + self._completed.add(ev["name"]) + elif ev["name"] not in existing_names: + self._today_schedule.append(ev) + injected += 1 + + self._today_schedule.sort(key=lambda e: (e["et_dt"], e.get("session", ""))) + self._log(f" βœ“ μ™„λ£Œ β€” μŠ€ν†±/EOD 이벀트 {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 ──────────────────────────────────────────────────── diff --git a/apps/web/routers/orb_trading.py b/apps/web/routers/orb_trading.py index 6b6ed85..f3b339c 100644 --- a/apps/web/routers/orb_trading.py +++ b/apps/web/routers/orb_trading.py @@ -169,8 +169,17 @@ async def run_session_today(session_id: str) -> dict[str, Any]: detail="μŠ€μΌ€μ€„λŸ¬κ°€ μ‹€ν–‰ 쀑이 μ•„λ‹™λ‹ˆλ‹€. λ¨Όμ € μžλ™ μŠ€μΌ€μ€„λŸ¬λ₯Ό μ‹œμž‘ν•˜μ„Έμš”.", ) - # Fire-and-forget β€” detection takes ~1–2 min; we return immediately - _asyncio.create_task(orb_auto_scheduler.run_session_now(session.session_name)) + # Fire-and-forget β€” detection takes ~1–2 min; we return immediately. + # 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 { "session_id": session.session_id,